-
Notifications
You must be signed in to change notification settings - Fork 2.7k
Add filtering by partnerGroupId to partner analytics #2829
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
The latest updates on your projects. Learn more about Vercel for GitHub.
|
WalkthroughAdds partner group propagation to link records: fetches programEnrollment.groupId, augments ExpandedLink with partnerGroupId, records partner_group_id to Tinybird, exposes groupId in analytics filters/pipes, renames a cron route to propagate updates, updates caching/recording calls, and adds a backfill migration. Changes
Sequence Diagram(s)sequenceDiagram
autonumber
participant Client
participant App as Web App
participant QStash
participant Cron as /api/cron/links/propagate-partner-link-updates
participant Redis
participant TB as Tinybird
Client->>App: Trigger partner/discount/group update
App->>QStash: publishJSON({ groupId }) to propagate-partner-link-updates
QStash->>Cron: POST (async)
par expire cache and record
Cron->>Redis: Expire related partner-link caches
Cron->>TB: recordLink(links with partner_group_id)
and
TB-->>Cron: ingestion result
Redis-->>Cron: ok
end
sequenceDiagram
autonumber
participant API as Link API (create/update/merge/transfer/tag delete)
participant DB as Prisma
participant TB as Tinybird
participant Redis
API->>DB: fetch/update link(s) including programEnrollment{groupId} / includeTags
DB-->>API: link(s) + programEnrollment.groupId
API->>Redis: cache link with partner/discount fields
API->>TB: recordLink({ ...link, partnerGroupId: programEnrollment?.groupId })
TB-->>API: ingest response
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60–90 minutes Possibly related PRs
Suggested reviewers
Pre-merge checks (3 passed)✅ Passed checks (3 passed)
Poem
✨ Finishing touches🧪 Generate unit tests
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Additional Comments:
apps/web/app/(ee)/api/cron/domains/delete/route.ts (lines 64-64):
The domain delete CRON job records links to Tinybird without fetching partner group data, causing missing partnerGroupId
values for partner links.
View Details
📝 Patch Details
diff --git a/apps/web/app/(ee)/api/cron/domains/delete/route.ts b/apps/web/app/(ee)/api/cron/domains/delete/route.ts
index adfc1a89a..5edcb761c 100644
--- a/apps/web/app/(ee)/api/cron/domains/delete/route.ts
+++ b/apps/web/app/(ee)/api/cron/domains/delete/route.ts
@@ -43,6 +43,11 @@ export async function POST(req: Request) {
tag: true,
},
},
+ programEnrollment: {
+ select: {
+ groupId: true,
+ },
+ },
},
take: 100,
orderBy: {
@@ -61,7 +66,12 @@ export async function POST(req: Request) {
linkCache.deleteMany(links),
// Record link in Tinybird
- recordLink(links),
+ recordLink(
+ links.map((link) => ({
+ ...link,
+ partnerGroupId: link.programEnrollment?.groupId || null,
+ }))
+ ),
// Remove image from R2 storage if it exists
links
Analysis
Domain Delete CRON Job Missing Partner Group Data
Issue Description
The domain delete CRON job at apps/web/app/(ee)/api/cron/domains/delete/route.ts
was recording link deletions to Tinybird without including partner group information, causing incomplete analytics data for partner links.
Root Cause Analysis
The Problem
The Prisma query that fetches links for deletion (lines 35-50) only included tags
relation but missing the programEnrollment
relation:
const links = await prisma.link.findMany({
where: { domain },
include: {
tags: {
select: { tag: true },
},
// Missing programEnrollment relation
},
});
Expected Data Structure
The recordLink
function expects an ExpandedLink
type that includes partnerGroupId?: string | null
. This field is populated from the programEnrollment.groupId
value, but the query wasn't fetching this relationship.
The Tinybird analytics schema includes partner_group_id
as a tracked field, which gets populated by the transformLinkTB
function:
export const transformLinkTB = (link: ExpandedLink) => {
return {
// ... other fields
partner_group_id: link.partnerGroupId ?? "",
// ... other fields
};
};
Data Model Relationships
From the Prisma schema:
Link
model has a compound relation toProgramEnrollment
:programEnrollment ProgramEnrollment? @relation(fields: [programId, partnerId], references: [programId, partnerId])
ProgramEnrollment
model hasgroupId
field that referencesPartnerGroup
PartnerGroup
contains partner segmentation data crucial for analytics
Impact
Data Inconsistency: Partner links being deleted were recorded to Tinybird with empty partner_group_id
values, making it impossible to:
- Track partner performance by group
- Generate accurate partner analytics reports
- Analyze conversion rates by partner segments
- Maintain data integrity in the analytics pipeline
Solution
The fix includes two changes to apps/web/app/(ee)/api/cron/domains/delete/route.ts
:
- Enhanced Query: Added
programEnrollment
relation withgroupId
selection:
include: {
tags: {
select: { tag: true },
},
programEnrollment: {
select: { groupId: true },
},
},
- Data Mapping: Updated the
recordLink
call to properly mappartnerGroupId
:
recordLink(
links.map((link) => ({
...link,
partnerGroupId: link.programEnrollment?.groupId || null,
}))
),
This ensures that when partner links are deleted, their group membership is correctly recorded in Tinybird analytics, maintaining data consistency across the partner program analytics pipeline.
apps/web/app/(ee)/api/cron/domains/transfer/route.ts (lines 83-89):
The domain transfer CRON job records links to Tinybird without fetching partner group data, causing missing partnerGroupId
values for partner links.
View Details
📝 Patch Details
diff --git a/apps/web/app/(ee)/api/cron/domains/transfer/route.ts b/apps/web/app/(ee)/api/cron/domains/transfer/route.ts
index e9586783e..076f332ef 100644
--- a/apps/web/app/(ee)/api/cron/domains/transfer/route.ts
+++ b/apps/web/app/(ee)/api/cron/domains/transfer/route.ts
@@ -29,6 +29,13 @@ export async function POST(req: Request) {
const links = await prisma.link.findMany({
where: { domain, projectId: currentWorkspaceId },
take: 100,
+ include: {
+ programEnrollment: {
+ select: {
+ groupId: true,
+ },
+ },
+ },
orderBy: {
createdAt: "desc",
},
@@ -85,6 +92,7 @@ export async function POST(req: Request) {
...link,
projectId: newWorkspaceId,
folderId: null,
+ partnerGroupId: link.programEnrollment?.groupId,
})),
),
]);
Analysis
Domain Transfer CRON Job Missing Partner Group Data
Bug Description
The domain transfer CRON job in apps/web/app/(ee)/api/cron/domains/transfer/route.ts
was not fetching partner group data when recording link metadata to Tinybird. This resulted in null
or missing partnerGroupId
values for partner links in analytics data.
Technical Analysis
Root Cause
The initial Prisma query (lines 29-35) fetched link records without including the programEnrollment
relationship:
const links = await prisma.link.findMany({
where: { domain, projectId: currentWorkspaceId },
take: 100,
orderBy: {
createdAt: "desc",
},
});
When recordLink()
was called (lines 83-89), the links being passed didn't have access to the partner group information:
recordLink(
links.map((link) => ({
...link,
projectId: newWorkspaceId,
folderId: null,
// Missing: partnerGroupId field
})),
)
Evidence from Codebase
The correct pattern is demonstrated in apps/web/scripts/migrations/backfill-partner-group-tb.ts
, which:
- Includes
programEnrollment
data in the Prisma query:
include: {
programEnrollment: {
select: {
groupId: true,
},
},
}
- Maps the partner group ID when calling
recordLink()
:
recordLink(
links.map((link) => ({
...link,
partnerGroupId: link.programEnrollment?.groupId,
})),
)
Impact
Data Consistency: Partner links transferred between domains would have missing partnerGroupId
values in Tinybird analytics, causing:
- Incomplete partner attribution data
- Inaccurate partner performance metrics
- Potential issues with partner commission calculations
Analytics Integrity: The missing partner group associations would create gaps in analytics queries that depend on partner group segmentation.
Solution
The fix adds the missing programEnrollment
include to the initial query and maps the partnerGroupId
field when calling recordLink()
:
- Enhanced Query: Include
programEnrollment
relationship to accessgroupId
- Proper Mapping: Extract
partnerGroupId
fromlink.programEnrollment?.groupId
when recording to Tinybird
This ensures partner links maintain their group associations during domain transfers, preserving analytics data integrity.
Files Modified
apps/web/app/(ee)/api/cron/domains/transfer/route.ts
: Added programEnrollment include and partnerGroupId mapping
apps/web/app/(ee)/api/cron/domains/update/route.ts (lines 82-82):
The domain update CRON job records links to Tinybird without fetching partner group data, causing missing partnerGroupId
values for partner links.
View Details
📝 Patch Details
diff --git a/apps/web/app/(ee)/api/cron/domains/update/route.ts b/apps/web/app/(ee)/api/cron/domains/update/route.ts
index 61707c974..1311f02a0 100644
--- a/apps/web/app/(ee)/api/cron/domains/update/route.ts
+++ b/apps/web/app/(ee)/api/cron/domains/update/route.ts
@@ -72,6 +72,11 @@ export async function POST(req: Request) {
tag: true,
},
},
+ programEnrollment: {
+ select: {
+ groupId: true,
+ },
+ },
},
});
@@ -79,7 +84,12 @@ export async function POST(req: Request) {
// update the `shortLink` field for each of the short links
updateShortLinks(updatedLinks),
// record new link values in Tinybird (dub_links_metadata)
- recordLink(updatedLinks),
+ recordLink(
+ updatedLinks.map((link) => ({
+ ...link,
+ partnerGroupId: link.programEnrollment?.groupId,
+ }))
+ ),
// expire the redis cache for the old links
linkCache.expireMany(linksToUpdate),
]);
Analysis
Domain Update CRON Job Missing Partner Group Data Bug
Summary
The domain update CRON job (apps/web/app/(ee)/api/cron/domains/update/route.ts
) was recording links to Tinybird without fetching partner group data, causing missing partnerGroupId
values for partner links and creating data inconsistency in analytics.
Technical Analysis
The Problem
The domain update CRON job performs three key operations when updating links from an old domain to a new domain:
- Updates the domain in the database
- Updates short link URLs
- Records updated links to Tinybird for analytics
However, the query to fetch updated links (lines 62-80) only included basic link data and tags:
const updatedLinks = await prisma.link.findMany({
where: { id: { in: linkIdsToUpdate } },
include: {
tags: { select: { tag: true } }
}
});
This query was missing the programEnrollment
relation needed to access partner group data.
Data Flow Impact
When recordLink(updatedLinks)
was called (line 82), partner links were missing their partnerGroupId
values because:
- The query didn't include
programEnrollment.groupId
- Links were passed directly to
recordLink()
without mapping the partner group data - The Tinybird schema transforms missing
partnerGroupId
to empty string""
Pattern Analysis Evidence
Multiple other operations in the codebase correctly handle partner group data:
create-link.ts
: UsesgetPartnerAndDiscount()
and passespartnerGroupId: partner?.groupId
update-link.ts
: UsesgetPartnerAndDiscount()
and passespartnerGroupId: partner?.groupId
propagate-partner-link-updates/route.ts
: Manually addspartnerGroupId: group.id
backfill-partner-group-tb.ts
: IncludesprogramEnrollment
in query and mapspartnerGroupId
The Fix
Updated the domain update CRON job to:
-
Include partner enrollment data in query:
include: { tags: { select: { tag: true } }, programEnrollment: { select: { groupId: true } } }
-
Map partner group data when recording to Tinybird:
recordLink( updatedLinks.map((link) => ({ ...link, partnerGroupId: link.programEnrollment?.groupId, })) )
Impact and Consequences
Before Fix
- Partner links updated during domain migrations had missing
partnerGroupId
in Tinybird - Analytics queries grouping by partner groups would exclude these links
- Partner attribution and commission tracking could be inaccurate
- Data inconsistency between different link operations
After Fix
- All partner links maintain proper group attribution during domain updates
- Consistent partner analytics across all link operations
- Accurate partner commission and attribution tracking
- Data integrity maintained in Tinybird analytics
Related Components
- Tinybird Schema:
dub_links_metadata
datasource withpartner_group_id
field - Link Analytics: Partner group attribution and reporting
- Partner Commissions: Group-based reward calculations
- Domain Migration: Bulk link updates preserving partner data
This fix ensures data consistency and prevents analytics gaps during domain migration operations.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 10
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (12)
packages/tinybird/pipes/v2_top_tags.pipe (2)
39-41
: Activate PREWHERE when only groupId is presentMirror other filters by adding
defined(groupId)
so queries prune viaworkspace_links_with_tags
.- {% if defined(workspaceId) or defined(partnerId) or defined(programId) %} + {% if defined(workspaceId) or defined(partnerId) or defined(programId) or defined(groupId) %} PREWHERE link_id in (SELECT link_id from workspace_links_with_tags) {% end %}Apply to all three locations above.
Also applies to: 99-101, 163-165
253-255
: Broken reference: PREWHERE uses non-existent node
filtered_clicks
referencesworkspace_links
, but this pipe definesworkspace_links_with_tags
. This will fail at compile/run time.- PREWHERE link_id in (SELECT link_id from workspace_links) + PREWHERE link_id in (SELECT link_id from workspace_links_with_tags)packages/tinybird/pipes/v2_os.pipe (1)
51-54
: Include groupId in PREWHERE gatingWithout this, queries with only
groupId
won’t prune byworkspace_links
.- {% elif not defined(linkId) and ( - defined(workspaceId) or defined(partnerId) or defined(programId) - ) %} PREWHERE link_id in (SELECT link_id from workspace_links) + {% elif not defined(linkId) and ( + defined(workspaceId) or defined(partnerId) or defined(programId) or defined(groupId) + ) %} PREWHERE link_id in (SELECT link_id from workspace_links)Apply at all shown locations.
Also applies to: 100-102, 143-145, 183-185
packages/tinybird/pipes/v2_regions.pipe (2)
48-50
: Add groupId to PREWHERE gatingEnable pruning when only
groupId
is specified.- {% if not defined(linkId) and (defined(workspaceId) or defined(partnerId) or defined(programId)) %} + {% if not defined(linkId) and (defined(workspaceId) or defined(partnerId) or defined(programId) or defined(groupId)) %} PREWHERE link_id in (SELECT link_id from workspace_links) {% end %}Apply to all four locations.
Also applies to: 93-95, 140-142, 182-184
40-45
: Syntax error: trailing comma before FROMThere’s a trailing comma after
clicks
/leads
, which breaks the SELECT list.- SELECT - CONCAT(country, '-', region) as region, - country, - clicks, + SELECT + CONCAT(country, '-', region) as region, + country, + clicks- SELECT - CONCAT(country, '-', region) as region, - country, - leads, + SELECT + CONCAT(country, '-', region) as region, + country, + leadsAlso applies to: 85-90
packages/tinybird/pipes/v2_countries.pipe (1)
51-53
: Enable PREWHERE when only groupId is providedAdd
defined(groupId)
to the gating to benefit from link pruning.- {% elif not defined(linkId) and (defined(workspaceId) or defined(partnerId) or defined(programId)) %} + {% elif not defined(linkId) and (defined(workspaceId) or defined(partnerId) or defined(programId) or defined(groupId)) %} PREWHERE link_id in (SELECT link_id from workspace_links)Apply to all listed locations.
Also applies to: 97-99, 149-151, 198-200
packages/tinybird/pipes/v2_events.pipe (1)
56-58
: Include groupId in gating for link-based filteringEvents should also respect
groupId
when it is the only scoping parameter.-{% elif defined(workspaceId) or defined(partnerId) or defined(programId) %} +{% elif defined(workspaceId) or defined(partnerId) or defined(programId) or defined(groupId) %} AND link_id IN (SELECT link_id FROM workspace_links)Apply in all four places above.
Also applies to: 107-109, 184-186, 263-265
packages/tinybird/pipes/v2_utms.pipe (1)
59-62
: Bug: groupId-only queries won’t use workspace_links PREWHERE.All PREWHERE guards exclude groupId, so a request with only groupId defined won’t filter by workspace_links. Add defined(groupId) to the guard.
Apply:
- {% elif not defined(linkId) and ( - defined(workspaceId) or defined(partnerId) or defined(programId) - ) %} PREWHERE link_id in (SELECT link_id from workspace_links) + {% elif not defined(linkId) and ( + defined(workspaceId) or defined(partnerId) or defined(programId) or defined(groupId) + ) %} PREWHERE link_id in (SELECT link_id from workspace_links)Repeat the same change in utms_leads, utms_sales, and utms_sales_with_type blocks.
Also applies to: 125-127, 191-194, 259-262
packages/tinybird/pipes/v2_triggers.pipe (1)
51-54
: Bug: groupId-only queries bypass PREWHERE.Mirror the other filters by including defined(groupId).
- {% elif not defined(linkId) and ( - defined(workspaceId) or defined(partnerId) or defined(programId) - ) %} PREWHERE link_id in (SELECT link_id from workspace_links) + {% elif not defined(linkId) and ( + defined(workspaceId) or defined(partnerId) or defined(programId) or defined(groupId) + ) %} PREWHERE link_id in (SELECT link_id from workspace_links)Apply to trigger_clicks, trigger_leads, trigger_sales, and trigger_sales_with_type.
Also applies to: 100-102, 146-148, 186-189
packages/tinybird/pipes/v2_continents.pipe (1)
51-54
: Bug: groupId-only queries bypass PREWHERE.Include defined(groupId) so group-scoped queries hit workspace_links.
- {% elif not defined(linkId) and ( - defined(workspaceId) or defined(partnerId) or defined(programId) - ) %} PREWHERE link_id in (SELECT link_id from workspace_links) + {% elif not defined(linkId) and ( + defined(workspaceId) or defined(partnerId) or defined(programId) or defined(groupId) + ) %} PREWHERE link_id in (SELECT link_id from workspace_links)Apply to clicks, leads, sales, and sales_with_type nodes.
Also applies to: 109-111, 161-163, 210-212
packages/tinybird/pipes/v2_count.pipe (1)
51-54
: Bug: groupId-only queries bypass PREWHERE.Add defined(groupId) alongside other scoping params.
- {% elif not defined(linkId) and ( - defined(workspaceId) or defined(partnerId) or defined(programId) - ) %} PREWHERE link_id in (SELECT link_id from workspace_links) + {% elif not defined(linkId) and ( + defined(workspaceId) or defined(partnerId) or defined(programId) or defined(groupId) + ) %} PREWHERE link_id in (SELECT link_id from workspace_links)Also applies to: 98-100, 136-139, 192-195
apps/web/lib/planetscale/get-partner-discount.ts (1)
3-14
: Type mismatch: LEFT JOIN’d Discount fields can be null.QueryResult types for discountId/amount/type/maxDuration/couponId/couponTestId should be nullable to reflect LEFT JOIN and avoid unsafe assumptions.
interface QueryResult { id: string; name: string; image: string | null; groupId: string | null; - discountId: string; - amount: number; - type: "percentage" | "flat"; - maxDuration: number | null; - couponId: string | null; - couponTestId: string | null; + discountId: string | null; + amount: number | null; + type: "percentage" | "flat" | null; + maxDuration: number | null; + couponId: string | null; + couponTestId: string | null; }Also safe as-is at runtime (guarded by result.discountId), but TS types should match the SQL.
♻️ Duplicate comments (1)
apps/web/lib/api/links/propagate-bulk-link-changes.ts (1)
18-38
: Bug: Assumes single programId; breaks partnerGroupId mapping for mixed-program batches.Needs composite (programId, partnerId) mapping to fetch correct enrollments.
- if (partnerLinks.length > 0) { - const programId = partnerLinks[0].programId!; - const uniquePartnerIds = [ - ...new Set(partnerLinks.map((link) => link.partnerId) as string[]), - ]; - - const enrollments = await prisma.programEnrollment.findMany({ - where: { - partnerId: { - in: uniquePartnerIds, - }, - programId, - }, - select: { - partnerId: true, - groupId: true, - }, - }); - - partnerGroupMap = new Map( - enrollments.map(({ partnerId, groupId }) => [partnerId, groupId]), - ); - } + if (partnerLinks.length > 0) { + // Unique (programId, partnerId) pairs + const pairs = [ + ...new Map( + partnerLinks.map((l) => [ + `${l.programId}:${l.partnerId}`, + { programId: l.programId!, partnerId: l.partnerId! }, + ]), + ).values(), + ]; + + const enrollments = await prisma.programEnrollment.findMany({ + where: { + OR: pairs.map(({ programId, partnerId }) => ({ programId, partnerId })), + }, + select: { + programId: true, + partnerId: true, + groupId: true, + }, + }); + + partnerGroupMap = new Map( + enrollments.map(({ programId, partnerId, groupId }) => [ + `${programId}:${partnerId}`, + groupId, + ]), + ); + }
🧹 Nitpick comments (28)
packages/tinybird/pipes/v2_top_tags.pipe (1)
269-281
: Normalize UTM encoding for consistency and correctnessOther pipes URL-encode UTM values. Do the same here to avoid mismatches on special characters.
- AND url LIKE concat('%utm_source=', {{ String(utm_source) }}, '%') + AND url LIKE concat('%utm_source=', encodeURLFormComponent({{ String(utm_source) }}), '%') - AND url LIKE concat('%utm_medium=', {{ String(utm_medium) }}, '%') + AND url LIKE concat('%utm_medium=', encodeURLFormComponent({{ String(utm_medium) }}), '%') - AND url LIKE concat('%utm_campaign=', {{ String(utm_campaign) }}, '%') + AND url LIKE concat('%utm_campaign=', encodeURLFormComponent({{ String(utm_campaign) }}), '%') - AND url LIKE concat('%utm_term=', {{ String(utm_term) }}, '%') + AND url LIKE concat('%utm_term=', encodeURLFormComponent({{ String(utm_term) }}), '%') - AND url LIKE concat('%utm_content=', {{ String(utm_content) }}, '%') + AND url LIKE concat('%utm_content=', encodeURLFormComponent({{ String(utm_content) }}), '%')apps/web/lib/actions/partners/update-discount.ts (1)
61-65
: Route rename to propagate-partner-link-updates — OK; guard null groupIdURL change looks correct. Consider skipping the QStash call if
partnerGroup?.id
is falsy.- ? qstash.publishJSON({ + ? (partnerGroup?.id + ? qstash.publishJSON({ url: `${APP_DOMAIN_WITH_NGROK}/api/cron/links/propagate-partner-link-updates`, body: { groupId: partnerGroup?.id, }, - }) + }) + : Promise.resolve()) : Promise.resolve(),Confirm the cron route treats
groupId
as required to avoid no-op fanouts.apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/groups/groups-table.tsx (1)
272-281
: Add “View analytics” menu item — nice UX winOptional: preserve current analytics filters (date range, interval) by forwarding existing query params when pushing.
- router.push( - `/${slug}/program/analytics?groupId=${row.original.id}`, - ) + router.push(`/${slug}/program/analytics?groupId=${row.original.id}`)If you want to retain filters, consider building from
useRouterStuff().getQueryString()
and merginggroupId
.packages/tinybird/pipes/v2_referer_urls.pipe (1)
2-2
: Fix description to reflect contentConsider “Top referer URLs” instead of “Top countries”.
packages/tinybird/pipes/v2_browsers.pipe (1)
2-2
: Tweak descriptionUse “Top browsers”.
packages/tinybird/pipes/v2_devices.pipe (1)
2-2
: Tweak descriptionUse “Top devices”.
packages/tinybird/pipes/v2_referers.pipe (1)
2-2
: Tweak descriptionUse “Top referrers”.
packages/tinybird/pipes/v2_triggers.pipe (2)
1-2
: Description mismatch (“Top countries”).Rename to “Triggers” to reflect the endpoint.
-DESCRIPTION > - Top countries +DESCRIPTION > + Triggers
169-170
: Minor: standardize DateTime vs DateTime64.Other pipes use DateTime64 for event timestamps; consider aligning for consistency/perf.
Also applies to: 227-228
packages/tinybird/pipes/v2_continents.pipe (1)
141-142
: Minor: unify timestamp casting.Mix of DateTime and DateTime64 across nodes; consider standardizing (prefer DateTime64).
Also applies to: 193-194, 242-243, 95-96
packages/tinybird/pipes/v2_count.pipe (2)
1-2
: Description mismatch (“Top countries”).Rename to “Counts” to reflect output.
-DESCRIPTION > - Top countries +DESCRIPTION > + Counts
86-87
: Minor: standardize DateTime vs DateTime64.Consider using DateTime64 consistently across endpoints.
Also applies to: 121-122, 177-178, 233-234
apps/web/app/(ee)/api/cron/folders/delete/route.ts (1)
55-60
: Ensure recordLink tolerates undefined partnerGroupId.recordLink should map undefined to null for partner_group_id; otherwise coerce explicitly.
- partnerGroupId: link.programEnrollment?.groupId, + partnerGroupId: link.programEnrollment?.groupId ?? null,apps/web/lib/planetscale/get-partner-discount.ts (1)
32-46
: Optional: alias Discount fields to avoid future name collisions.Aliasing (e.g., Discount.type AS discountType) can prevent clashes if more tables/fields are added.
apps/web/lib/api/links/complete-ab-tests.ts (1)
83-86
: Avoid passing programEnrollment to recordLink payload (potential excess property/type mismatch).Prefer stripping programEnrollment before forwarding to Tinybird.
Apply:
- recordLink({ - ...response, - partnerGroupId: response.programEnrollment?.groupId, - }), + // Keep TB payload lean and TS-safe + (() => { + const { programEnrollment, ...rest } = response as any; + return recordLink({ + ...rest, + partnerGroupId: programEnrollment?.groupId ?? null, + }); + })(),apps/web/lib/partners/approve-partner-enrollment.ts (1)
175-181
: Always record the enrolled link with partnerGroupId (handles both updated and newly created link).Unify the call so we also record when partnerLink is created via createPartnerLink.
Apply:
- updatedLink - ? recordLink({ - ...updatedLink, - partnerGroupId: group.id, - }) - : Promise.resolve(null), + recordLink({ + ...(updatedLink ?? partnerLink), + partnerGroupId: group.id, + }),apps/web/app/(ee)/api/cron/merge-partner-accounts/route.ts (1)
174-190
: Propagating partnerGroupId to TB after merge is correct; consider chunked ingestion.Including programEnrollment.groupId and mapping partnerGroupId is spot on. If updatedLinks can be large, chunk the TB ingest to avoid 413s/timeouts.
Apply:
- recordLink( - updatedLinks.map((link) => ({ - ...link, - partnerGroupId: link.programEnrollment?.groupId, - })), - ), + (async () => { + const payload = updatedLinks.map((link) => ({ + ...link, + partnerGroupId: link.programEnrollment?.groupId, + })); + const CHUNK = 500; + for (let i = 0; i < payload.length; i += CHUNK) { + await recordLink(payload.slice(i, i + CHUNK)); + } + })(),Also applies to: 193-200
apps/web/app/api/tags/[id]/route.ts (1)
101-105
: Normalize undefined to null for consistency with TB schemas.Avoid sending
undefined
(which may be omitted during serialization). Prefer explicitnull
.Apply:
- partnerGroupId: link.programEnrollment?.groupId, + partnerGroupId: link.programEnrollment?.groupId ?? null,apps/web/lib/api/links/create-link.ts (2)
139-145
: Don’t let partner lookup failure block all post-create tasks.If
getPartnerAndDiscount
throws (DB/network), none of the subsequent tasks run. Catch and default tonull
so caching, Tinybird, image, qstash, usage updates, etc., still proceed.Apply:
- (async () => { - const { partner, discount } = await getPartnerAndDiscount({ - programId: response.programId, - partnerId: response.partnerId, - }); + (async () => { + let partner: Awaited<ReturnType<typeof getPartnerAndDiscount>>["partner"] | null = null; + let discount: Awaited<ReturnType<typeof getPartnerAndDiscount>>["discount"] | null = null; + try { + ({ partner, discount } = await getPartnerAndDiscount({ + programId: response.programId, + partnerId: response.partnerId, + })); + } catch { + // swallow; proceed without partner/discount + }
154-157
: Send explicit null for partnerGroupId.Tinybird/transformers typically expect nullables; avoid
undefined
.Apply:
- recordLink({ - ...response, - partnerGroupId: partner?.groupId, - }), + recordLink({ + ...response, + partnerGroupId: partner?.groupId ?? null, + }),apps/web/scripts/migrations/backfill-partner-group-tb.ts (1)
51-56
: Normalize partnerGroupId to null.Guarantee a concrete nullable for ingestion.
Apply:
- links.map((link) => ({ - ...link, - partnerGroupId: link.programEnrollment?.groupId, - })), + links.map((link) => ({ + ...link, + partnerGroupId: link.programEnrollment?.groupId ?? null, + })),apps/web/app/(ee)/api/cron/links/propagate-partner-link-updates/route.ts (2)
37-40
: Return 204 No Content for missing group to avoid retries.Explicit 204 makes intent clearer for job runners while keeping idempotency.
- console.error(`Group ${groupId} not found.`); - return new Response("OK"); + console.error(`Group ${groupId} not found.`); + return new Response(null, { status: 204 });
78-91
: Surface failures from Promise.allSettled to avoid silent data loss.Currently failures are swallowed; log and keep going per chunk.
- await Promise.allSettled([ + const results = await Promise.allSettled([ // Expire the cache for the links linkCache.expireMany( linkChunk.map(({ domain, key }) => ({ domain, key })), ), // Record the updated links in Tinybird recordLink( linkChunk.map((link) => ({ ...link, partnerGroupId: group.id, })), ), ]); + for (const r of results) { + if (r.status === "rejected") { + console.error("propagate-partner-link-updates error:", r.reason); + } + }apps/web/lib/api/links/propagate-bulk-link-changes.ts (2)
47-53
: Coalesce undefined to null for Tinybird schema consistency.
Map.get
can returnundefined
. Coalesce tonull
to match nullable TB column.- partnerGroupId: link.partnerId - ? partnerGroupMap.get(link.partnerId) - : null, + partnerGroupId: + link.partnerId && link.programId + ? partnerGroupMap.get(`${link.programId}:${link.partnerId}`) ?? null + : null,
41-54
: Optional: Avoid non-Promise values in Promise.all.Minor readability: filter out falsy entry when
skipRedisCache
is true.-return await Promise.all([ - // update Redis cache - !skipRedisCache && linkCache.mset(links), - // update Tinybird - recordLink(/* ... */), -]); +const tasks: Promise<unknown>[] = []; +if (!skipRedisCache) tasks.push(linkCache.mset(links)); +tasks.push( + recordLink( + links.map((link) => ({ + ...link, + partnerGroupId: + link.partnerId && link.programId + ? partnerGroupMap.get(`${link.programId}:${link.partnerId}`) ?? null + : null, + })), + ), +); +return await Promise.all(tasks);apps/web/ui/analytics/use-analytics-filters.tsx (1)
523-537
: Use JSX for the group icon for consistency and clearer TSX semantics.Calling the component as a function works but is atypical.
- options: loadingGroups - ? null - : groups?.map((group) => ({ + options: loadingGroups + ? null + : groups?.map((group) => ({ value: group.id, label: group.name, - icon: GroupColorCircle({ group }), - right: nFormatter(group.partners, { + icon: <GroupColorCircle group={group} />, + right: nFormatter( + // tolerate differing shapes across queries + (typeof group.partners === "number" + ? group.partners + : (group as any).partnerCount ?? + (group as any)._count?.partners ?? + 0) as number, + { full: true, - }), + }, + ), })) ?? null,apps/web/lib/api/links/update-link.ts (2)
177-217
: Filter out non-promises in Promise.allSettled.Several array entries can be boolean. Filtering improves clarity and avoids no-op settled values.
- await Promise.allSettled([ + await Promise.allSettled( + [ // cache link in Redis linkCache.set({ ...response, ...(partner && { partner }), ...(discount && { discount }), }), @@ testVariants && testCompletedAt && scheduleABTestCompletion(response), - ]); + ].filter(Boolean) as Promise<unknown>[] + );
203-207
: Make R2 deletion robust to trailing slashes by parsing URL.String replace can break if
R2_URL
has a trailing slash or differs by scheme/host casing. Extract the path instead.- storage.delete(oldLink.image.replace(`${R2_URL}/`, "")), + storage.delete(new URL(oldLink.image).pathname.slice(1)),
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (47)
apps/web/app/(ee)/api/cron/folders/delete/route.ts
(2 hunks)apps/web/app/(ee)/api/cron/links/propagate-partner-link-updates/route.ts
(4 hunks)apps/web/app/(ee)/api/cron/merge-partner-accounts/route.ts
(1 hunks)apps/web/app/(ee)/api/events/route.ts
(1 hunks)apps/web/app/(ee)/api/groups/[groupIdOrSlug]/partners/route.ts
(1 hunks)apps/web/app/(ee)/api/groups/[groupIdOrSlug]/route.ts
(1 hunks)apps/web/app/api/links/[linkId]/transfer/route.ts
(1 hunks)apps/web/app/api/tags/[id]/route.ts
(2 hunks)apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/groups/groups-table.tsx
(2 hunks)apps/web/lib/actions/partners/create-discount.ts
(1 hunks)apps/web/lib/actions/partners/delete-discount.ts
(1 hunks)apps/web/lib/actions/partners/update-discount.ts
(1 hunks)apps/web/lib/analytics/constants.ts
(1 hunks)apps/web/lib/api/links/complete-ab-tests.ts
(2 hunks)apps/web/lib/api/links/create-link.ts
(1 hunks)apps/web/lib/api/links/propagate-bulk-link-changes.ts
(2 hunks)apps/web/lib/api/links/update-link.ts
(1 hunks)apps/web/lib/api/links/utils/transform-link.ts
(1 hunks)apps/web/lib/api/partners/create-and-enroll-partner.ts
(1 hunks)apps/web/lib/partners/approve-partner-enrollment.ts
(1 hunks)apps/web/lib/planetscale/get-partner-discount.ts
(3 hunks)apps/web/lib/swr/use-groups.ts
(1 hunks)apps/web/lib/tinybird/record-link.ts
(2 hunks)apps/web/lib/zod/schemas/analytics.ts
(2 hunks)apps/web/scripts/migrations/backfill-partner-group-tb.ts
(1 hunks)apps/web/ui/analytics/use-analytics-filters.tsx
(9 hunks)packages/tinybird/datasources/dub_links_metadata.datasource
(1 hunks)packages/tinybird/datasources/dub_links_metadata_latest.datasource
(1 hunks)packages/tinybird/datasources/dub_regular_links_metadata_latest.datasource
(1 hunks)packages/tinybird/pipes/v2_browsers.pipe
(1 hunks)packages/tinybird/pipes/v2_cities.pipe
(1 hunks)packages/tinybird/pipes/v2_continents.pipe
(1 hunks)packages/tinybird/pipes/v2_count.pipe
(1 hunks)packages/tinybird/pipes/v2_countries.pipe
(1 hunks)packages/tinybird/pipes/v2_devices.pipe
(1 hunks)packages/tinybird/pipes/v2_events.pipe
(1 hunks)packages/tinybird/pipes/v2_os.pipe
(1 hunks)packages/tinybird/pipes/v2_referer_urls.pipe
(1 hunks)packages/tinybird/pipes/v2_referers.pipe
(1 hunks)packages/tinybird/pipes/v2_regions.pipe
(1 hunks)packages/tinybird/pipes/v2_timeseries.pipe
(1 hunks)packages/tinybird/pipes/v2_top_links.pipe
(1 hunks)packages/tinybird/pipes/v2_top_partners.pipe
(1 hunks)packages/tinybird/pipes/v2_top_tags.pipe
(1 hunks)packages/tinybird/pipes/v2_top_urls.pipe
(1 hunks)packages/tinybird/pipes/v2_triggers.pipe
(1 hunks)packages/tinybird/pipes/v2_utms.pipe
(1 hunks)
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-06-06T07:59:03.120Z
Learnt from: devkiran
PR: dubinc/dub#2177
File: apps/web/lib/api/links/bulk-create-links.ts:66-84
Timestamp: 2025-06-06T07:59:03.120Z
Learning: In apps/web/lib/api/links/bulk-create-links.ts, the team accepts the risk of potential undefined results from links.find() operations when building invalidLinks arrays, because existing links are fetched from the database based on the input links, so matches are expected to always exist.
Applied to files:
apps/web/app/(ee)/api/cron/merge-partner-accounts/route.ts
apps/web/app/(ee)/api/cron/folders/delete/route.ts
apps/web/scripts/migrations/backfill-partner-group-tb.ts
apps/web/app/(ee)/api/cron/links/propagate-partner-link-updates/route.ts
apps/web/lib/api/links/create-link.ts
apps/web/lib/api/links/update-link.ts
apps/web/lib/api/links/propagate-bulk-link-changes.ts
🧬 Code graph analysis (18)
apps/web/app/(ee)/api/cron/merge-partner-accounts/route.ts (1)
apps/web/lib/tinybird/record-link.ts (1)
recordLink
(78-84)
apps/web/lib/swr/use-groups.ts (2)
apps/web/lib/zod/schemas/groups.ts (1)
getGroupsQuerySchema
(71-94)apps/web/lib/types.ts (1)
GroupExtendedProps
(528-528)
apps/web/lib/api/links/complete-ab-tests.ts (1)
apps/web/lib/tinybird/record-link.ts (1)
recordLink
(78-84)
apps/web/lib/partners/approve-partner-enrollment.ts (1)
apps/web/lib/tinybird/record-link.ts (1)
recordLink
(78-84)
apps/web/lib/actions/partners/delete-discount.ts (1)
packages/utils/src/constants/main.ts (1)
APP_DOMAIN_WITH_NGROK
(20-25)
apps/web/lib/actions/partners/update-discount.ts (1)
packages/utils/src/constants/main.ts (1)
APP_DOMAIN_WITH_NGROK
(20-25)
apps/web/app/(ee)/api/groups/[groupIdOrSlug]/route.ts (1)
packages/utils/src/constants/main.ts (1)
APP_DOMAIN_WITH_NGROK
(20-25)
apps/web/scripts/migrations/backfill-partner-group-tb.ts (1)
apps/web/lib/tinybird/record-link.ts (1)
recordLink
(78-84)
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/groups/groups-table.tsx (1)
packages/ui/src/menu-item.tsx (1)
MenuItem
(43-86)
apps/web/app/(ee)/api/cron/links/propagate-partner-link-updates/route.ts (2)
apps/web/lib/api/links/cache.ts (1)
linkCache
(113-113)apps/web/lib/tinybird/record-link.ts (1)
recordLink
(78-84)
apps/web/lib/api/partners/create-and-enroll-partner.ts (1)
apps/web/lib/tinybird/record-link.ts (1)
recordLink
(78-84)
apps/web/lib/actions/partners/create-discount.ts (1)
packages/utils/src/constants/main.ts (1)
APP_DOMAIN_WITH_NGROK
(20-25)
apps/web/app/api/links/[linkId]/transfer/route.ts (1)
apps/web/lib/tinybird/record-link.ts (1)
recordLink
(78-84)
apps/web/app/(ee)/api/groups/[groupIdOrSlug]/partners/route.ts (1)
packages/utils/src/constants/main.ts (1)
APP_DOMAIN_WITH_NGROK
(20-25)
apps/web/ui/analytics/use-analytics-filters.tsx (2)
apps/web/lib/swr/use-groups.ts (1)
useGroups
(10-38)apps/web/ui/partners/groups/group-color-circle.tsx (1)
GroupColorCircle
(5-24)
apps/web/lib/api/links/create-link.ts (3)
apps/web/lib/planetscale/get-partner-discount.ts (1)
getPartnerAndDiscount
(17-78)apps/web/lib/tinybird/record-link.ts (1)
recordLink
(78-84)packages/utils/src/constants/main.ts (1)
APP_DOMAIN_WITH_NGROK
(20-25)
apps/web/lib/api/links/update-link.ts (4)
apps/web/lib/planetscale/get-partner-discount.ts (1)
getPartnerAndDiscount
(17-78)apps/web/lib/api/links/cache.ts (1)
linkCache
(113-113)apps/web/lib/tinybird/record-link.ts (1)
recordLink
(78-84)packages/utils/src/constants/main.ts (1)
R2_URL
(81-81)
apps/web/lib/api/links/propagate-bulk-link-changes.ts (2)
apps/web/lib/api/links/cache.ts (1)
linkCache
(113-113)apps/web/lib/tinybird/record-link.ts (1)
recordLink
(78-84)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
- GitHub Check: Vade Review
- GitHub Check: build
🔇 Additional comments (42)
packages/tinybird/pipes/v2_top_urls.pipe (2)
21-21
: Add groupId filter — looks goodThe new
partner_group_id
predicate is correct and aligns with the rest of the filters.
43-45
: Ensure groupId activates PREWHERE pruningInclude defined(groupId) in the PREWHERE gating so queries with only groupId trigger PREWHERE and avoid full table scans.
Apply:
- {% if not defined(linkId) and (defined(workspaceId) or defined(partnerId) or defined(programId)) %} + {% if not defined(linkId) and (defined(workspaceId) or defined(partnerId) or defined(programId) or defined(groupId)) %} PREWHERE link_id in (SELECT link_id from workspace_links) {% end %}Also apply the same change in packages/tinybird/pipes/v2_top_urls.pipe at the other occurrences (lines 81-83, 125-127, 166-169). rg search returned no matches; unable to verify occurrences — re-run the repo-wide check or confirm file paths.
packages/tinybird/pipes/v2_top_tags.pipe (1)
18-18
: Add groupId filter — looks goodPredicate on
partner_group_id
is correct inworkspace_links_with_tags
.packages/tinybird/pipes/v2_os.pipe (1)
21-21
: Add groupId filter — looks goodCorrect placement alongside partner/program filters.
packages/tinybird/pipes/v2_regions.pipe (1)
21-21
: Add groupId filter — looks goodConsistent with other pipes.
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/groups/groups-table.tsx (1)
23-31
: Icon import add — OKImporting
LinesY
is correct for the new menu item.packages/tinybird/pipes/v2_countries.pipe (1)
21-21
: Add groupId filter — looks goodFilter is consistent with other fields.
packages/tinybird/pipes/v2_events.pipe (1)
21-21
: Add groupId filter — looks goodCorrectly extends
workspace_links
.packages/tinybird/pipes/v2_top_partners.pipe (1)
12-12
: LGTM: groupId filter on partner_linksThe conditional filter on partner_group_id is correctly scoped at the source join.
apps/web/lib/actions/partners/delete-discount.ts (1)
62-65
: ```shell
#!/bin/bash
set -euo pipefailecho "1) Search for occurrences of 'propagate-partner-link-updates' under apps/web"
rg -n --hidden -uu 'propagate-partner-link-updates' apps/web -S || trueecho
echo "2) Locate containing directories and print route.ts if present"
files=$(rg -n --hidden -uu 'propagate-partner-link-updates' apps/web -S --files-with-matches || true)
if [ -z "$files" ]; then
echo "No files found containing 'propagate-partner-link-updates'"
else
dirname -z $files 2>/dev/null | tr '\0' '\n' | sort -u | while read -r d; do
echo
echo "DIR: $d"
if [ -f "$d/route.ts" ]; then
echo "---- $d/route.ts (first 400 lines) ----"
sed -n '1,400p' "$d/route.ts" || true
else
echo "No route.ts in $d; listing contents:"
ls -la "$d" || true
fi
done
fiecho
echo "3) Show handler/exports in any matching route.ts files (look for POST/GET/handler)"
rg -nC3 --hidden -uu '\b(export (const|async function)\s+(POST|GET|handler)|export default function|export default)' apps/web -S || trueecho
echo "4) Search for 'groupId' usages under apps/web and show context"
rg -nC3 '\bgroupId\b' apps/web -S || trueecho
echo "5) Show the delete-discount action around the snippet you referenced"
if [ -f "apps/web/lib/actions/partners/delete-discount.ts" ]; then
sed -n '1,240p' apps/web/lib/actions/partners/delete-discount.ts || true
else
echo "File not found: apps/web/lib/actions/partners/delete-discount.ts"
fiecho
echo "6) Search for references to the old endpoint 'invalidate-for-discounts'"
rg -n --hidden -uu 'invalidate-for-discounts' -S || true</blockquote></details> <details> <summary>apps/web/lib/api/links/utils/transform-link.ts (2)</summary><blockquote> `17-18`: **LGTM: ExpandedLink now carries partnerGroupId** Type extension is non-breaking; transformLink preserves it via spread. Ensure downstream Tinybird mapping uses this field. --- `17-18`: **Confirmed: partnerGroupId → partner_group_id mapping exists** apps/web/lib/tinybird/record-link.ts defines partner_group_id (z.string().nullish()) and populates it with link.partnerGroupId ?? "" (see lines ~29 and ~72). </blockquote></details> <details> <summary>packages/tinybird/pipes/v2_utms.pipe (1)</summary><blockquote> `15-16`: **Good: added partner_group_id filter.** Conditional group filter matches existing style and placement. </blockquote></details> <details> <summary>packages/tinybird/pipes/v2_triggers.pipe (1)</summary><blockquote> `21-22`: **Good: added partner_group_id filter.** </blockquote></details> <details> <summary>packages/tinybird/pipes/v2_continents.pipe (1)</summary><blockquote> `21-22`: **Good: added partner_group_id filter.** </blockquote></details> <details> <summary>packages/tinybird/pipes/v2_count.pipe (1)</summary><blockquote> `21-22`: **Good: added partner_group_id filter.** </blockquote></details> <details> <summary>apps/web/app/(ee)/api/events/route.ts (1)</summary><blockquote> `20-31`: **Good: switch to const destructuring.** Improves immutability; no behavior change. </blockquote></details> <details> <summary>apps/web/app/(ee)/api/cron/folders/delete/route.ts (1)</summary><blockquote> `34-41`: **Include programEnrollment for partnerGroupId — good; verify no include clash.** If includeTags already sets programEnrollment, the spread could conflict. Confirm includeTags content or merge thoughtfully. Would you check includeTags to ensure it doesn’t already define programEnrollment? </blockquote></details> <details> <summary>apps/web/lib/planetscale/get-partner-discount.ts (1)</summary><blockquote> `60-77`: **Good: partner now includes groupId.** </blockquote></details> <details> <summary>apps/web/lib/analytics/constants.ts (1)</summary><blockquote> `175-176`: **Good: groupId added to VALID_ANALYTICS_FILTERS.** Matches TB changes and schema updates in this PR. </blockquote></details> <details> <summary>apps/web/lib/api/partners/create-and-enroll-partner.ts (1)</summary><blockquote> `178-181`: **Passing partnerGroupId into Tinybird payload looks correct.** group.id is already validated via getGroupOrThrow; adding partnerGroupId here should unblock group-level analytics. No further action. </blockquote></details> <details> <summary>packages/tinybird/datasources/dub_regular_links_metadata_latest.datasource (1)</summary><blockquote> `16-17`: **Schema extension to include partner_group_id looks good.** Ensure ingestion always sends a string (empty string when unknown) to avoid nullable drift in TB. </blockquote></details> <details> <summary>apps/web/lib/api/links/complete-ab-tests.ts (1)</summary><blockquote> `70-75`: **Including programEnrollment.groupId in the update response is appropriate.** Keeps the source-of-truth for partnerGroupId close to the write. </blockquote></details> <details> <summary>packages/tinybird/pipes/v2_cities.pipe (1)</summary><blockquote> `21-21`: **groupId filter added — LGTM.** Matches the pattern in other pipes and will scope workspace_links correctly. </blockquote></details> <details> <summary>packages/tinybird/datasources/dub_links_metadata.datasource (1)</summary><blockquote> `15-17`: **Added partner_group_id to ingestion schema — LGTM.** json path and column ordering mirror latest/regular sources. Confirm the transformer sets a default "" when absent. </blockquote></details> <details> <summary>packages/tinybird/datasources/dub_links_metadata_latest.datasource (1)</summary><blockquote> `16-17`: **Latest datasource includes partner_group_id — good alignment.** No engine/partition changes needed. Confirm downstream pipes reference this column (they do via workspace_links). </blockquote></details> <details> <summary>apps/web/app/(ee)/api/groups/[groupIdOrSlug]/partners/route.ts (1)</summary><blockquote> `52-52`: **Switched QStash target to propagate-partner-link-updates — good routing.** Please verify the new cron route is deployed across envs and is idempotent for repeated partnerIds. </blockquote></details> <details> <summary>apps/web/app/api/links/[linkId]/transfer/route.ts (1)</summary><blockquote> `96-99`: **Clearing partnerGroupId on transfer: good; verify partner/program invariants on cross-workspace moves.** Setting partnerGroupId: null when transferring avoids stale group analytics in TB. Please double-check whether programId/partnerId should also be cleared or remapped when projectId changes, to prevent mismatched analytics and FK drift across workspaces. Would you like a quick script to scan for links whose projectId no longer matches the owning program’s workspace? </blockquote></details> <details> <summary>apps/web/app/(ee)/api/groups/[groupIdOrSlug]/route.ts (1)</summary><blockquote> `215-237`: **Endpoint switch and settlement handling look good; confirm downstream job behavior.** Good move to /api/cron/links/propagate-partner-link-updates with Promise.allSettled. Please confirm that the cron route propagates the defaultGroup reassignment to Tinybird (including partner_group_id) for affected links. </blockquote></details> <details> <summary>apps/web/lib/swr/use-groups.ts (1)</summary><blockquote> `4-4`: **Type widening to GroupExtendedProps: looks fine; validate callsites.** Compile-time change only; ensure consumers expecting GroupProps still type-check. Also applies to: 10-10 </blockquote></details> <details> <summary>apps/web/lib/actions/partners/create-discount.ts (1)</summary><blockquote> `69-75`: **Propagation endpoint update: LGTM.** Publishing to propagate-partner-link-updates with groupId keeps links in sync post-discount creation; audit log async handling is solid. Also applies to: 77-91 </blockquote></details> <details> <summary>apps/web/lib/zod/schemas/analytics.ts (1)</summary><blockquote> `117-120`: **groupId filter added: LGTM; confirm pipes support and doc interplay with partnerId.** Schema changes look consistent. Please ensure TB pipes filter by partner_group_id when groupId is present and clarify in docs whether partnerId and groupId can be combined and how precedence works. Also applies to: 319-319 </blockquote></details> <details> <summary>apps/web/lib/tinybird/record-link.ts (1)</summary><blockquote> `29-32`: **partner_group_id added to schema and transform: LGTM; verify DS/pipes alignment.** Nullish-to-empty-string normalization matches existing fields. Confirm datasources and pipes now include partner_group_id and the backfill is complete before enabling groupId analytics by default. Also applies to: 72-72 </blockquote></details> <details> <summary>apps/web/app/api/tags/[id]/route.ts (1)</summary><blockquote> `79-83`: **Prisma include looks good and minimal.** Selecting only `programEnrollment.groupId` keeps the payload lean while enabling the downstream enrichment. </blockquote></details> <details> <summary>apps/web/lib/api/links/create-link.ts (1)</summary><blockquote> `146-151`: **LGTM on caching enrichment.** Augmenting cache with `partner`/`discount` is fine and keeps reads fast. </blockquote></details> <details> <summary>apps/web/app/(ee)/api/cron/links/propagate-partner-link-updates/route.ts (1)</summary><blockquote> `52-58`: **LGTM on fetching links with tags.** Including `...includeTags` aligns with Tinybird payload needs. No issues. </blockquote></details> <details> <summary>apps/web/ui/analytics/use-analytics-filters.tsx (4)</summary><blockquote> `137-143`: **Good: Groups fetched only on program pages.** Scoped data fetching minimizes overhead elsewhere. --- `201-226`: **LGTM: Adds groupId to active filters with proper special-casing.** Destructuring `groupId` out of `searchParamsObj` prevents double-inclusion in the dynamic pass. --- `877-898`: **Dependencies include groups/loadingGroups, preventing stale options.** Memo invalidation looks correct. --- `528-535`: **No change required — group.partners exists** GroupSchemaExtended (apps/web/lib/zod/schemas/groups.ts) defines partners and partnersCount, so using group.partners is valid; the example "partnerCount" is incorrect. </blockquote></details> <details> <summary>apps/web/lib/api/links/update-link.ts (2)</summary><blockquote> `171-176`: **Good consolidation of partner/discount fetch.** Up-front retrieval avoids duplicate queries later and keeps the post-update flow lean. --- `185-189`: **Tinybird payload enrichment looks right.** Passing `partnerGroupId` derived from the fetched partner aligns with the new analytics filter. </blockquote></details> </blockquote></details> </details> <!-- This is an auto-generated comment by CodeRabbit for review status -->
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 1
♻️ Duplicate comments (5)
packages/tinybird/pipes/v2_devices.pipe (1)
21-21
: Devices pipeline: groupId support — LGTMAddresses the earlier gating gap; PREWHERE now includes groupId across all nodes.
Also applies to: 52-54, 100-102, 143-145, 184-185
packages/tinybird/pipes/v2_regions.pipe (1)
21-21
: Cast groupId to String for consistency with ID columns.Mirror the casting used elsewhere (e.g., linkId/customerId) to avoid quoting issues.
- {% if defined(groupId) %} AND partner_group_id = {{ groupId }} {% end %} + {% if defined(groupId) %} AND partner_group_id = {{ String(groupId) }} {% end %}packages/tinybird/pipes/v2_utms.pipe (1)
15-15
: Cast groupId to String to match column type and escape correctly.- {% if defined(groupId) %} AND partner_group_id = {{ groupId }} {% end %} + {% if defined(groupId) %} AND partner_group_id = {{ String(groupId) }} {% end %}packages/tinybird/pipes/v2_timeseries.pipe (1)
134-137
: PREWHERE gate now considers groupId — resolvedThis addresses the earlier gating gap for clicks when only groupId is provided.
packages/tinybird/pipes/v2_referers.pipe (1)
51-54
: Clicks PREWHERE gate includes groupId — LGTMThis resolves the previously flagged issue for referers_clicks.
🧹 Nitpick comments (7)
packages/tinybird/pipes/v2_top_tags.pipe (1)
39-41
: Include groupId in PREWHERE gates for performance parityWithout adding groupId, queries scoped only by groupId won’t prefilter events and may scan far more rows than needed.
Apply in each block:
-{% if defined(workspaceId) or defined(partnerId) or defined(programId) %} +{% if defined(workspaceId) or defined(partnerId) or defined(programId) or defined(groupId) %} PREWHERE link_id in (SELECT link_id from workspace_links_with_tags) {% end %}Also applies to: 99-101, 163-165
packages/tinybird/pipes/v2_count.pipe (2)
51-54
: Adding groupId to PREWHERE gating: LGTM; optional: combine with customerId when both are provided.Current elif skips workspace_links gating when customerId is also set. You can conjunct both predicates to narrow scans further.
Suggested refactor:
- {% if defined(customerId) %} - PREWHERE click_id IN ( + {% if defined(customerId) %} + PREWHERE click_id IN ( SELECT DISTINCT click_id FROM dub_lead_events_mv WHERE customer_id = {{ String(customerId) }} - ) + ){% if not defined(linkId) and (defined(workspaceId) or defined(partnerId) or defined(programId) or defined(groupId)) %} + AND link_id in (SELECT link_id from workspace_links){% endif %} - {% elif not defined(linkId) and ( + {% elif not defined(linkId) and ( defined(workspaceId) or defined(partnerId) or defined(programId) or defined(groupId) ) %} PREWHERE link_id in (SELECT link_id from workspace_links)
21-21
: Cast groupId to String (partner_group_id is String).
- partner_group_id is declared as String in:
packages/tinybird/datasources/dub_links_metadata.datasource
packages/tinybird/datasources/dub_links_metadata_latest.datasource
packages/tinybird/datasources/dub_regular_links_metadata_latest.datasource- Apply diff:
- {% if defined(groupId) %} AND partner_group_id = {{ groupId }} {% end %} + {% if defined(groupId) %} AND partner_group_id = {{ String(groupId) }} {% end %}
- No ORDER BY / PRIMARY KEY matches found in those datasource files; consider adding an ORDER BY/PRIMARY KEY that includes partner_group_id to improve filtering performance.
packages/tinybird/pipes/v2_utms.pipe (1)
60-62
: Group PREWHERE gating in utms_clicks: LGTM; optional: also conjunct when customerId is set.Same optional combination as count_clicks to reduce scanned rows when both scopes apply.
packages/tinybird/pipes/v2_timeseries.pipe (2)
128-137
: Clicks with customerId bypasses group scoping — confirm intentWhen customerId is defined, the query uses click_id IN (leads...) and skips link_id prefiltering, so groupId/workspaceId/partnerId/programId are ignored. If you want customerId + groupId to intersect, add the workspace_links constraint inside the subquery.
Proposed patch:
- {% if defined(customerId) %} - PREWHERE click_id IN ( - SELECT DISTINCT click_id - FROM dub_lead_events_mv - WHERE customer_id = {{ String(customerId) }} - ) + {% if defined(customerId) %} + PREWHERE click_id IN ( + SELECT DISTINCT click_id + FROM dub_lead_events_mv + WHERE customer_id = {{ String(customerId) }} + {% if defined(groupId) or defined(workspaceId) or defined(partnerId) or defined(programId) %} + AND link_id in (SELECT link_id from workspace_links) + {% end %} + )
254-256
: Nit: missing descriptionNode description shows “undefined”. Optional: add a brief description for sales node for clarity in TB UI.
packages/tinybird/pipes/v2_referers.pipe (1)
45-54
: Clicks with customerId bypasses group scoping — mirror fix here if desiredSame behavior as timeseries: customerId path ignores groupId/workspaceId/partnerId/programId. If intersection is desired, replicate the subquery filter.
Proposed patch:
- {% if defined(customerId) %} - PREWHERE click_id IN ( - SELECT DISTINCT click_id - FROM dub_lead_events_mv - WHERE customer_id = {{ String(customerId) }} - ) + {% if defined(customerId) %} + PREWHERE click_id IN ( + SELECT DISTINCT click_id + FROM dub_lead_events_mv + WHERE customer_id = {{ String(customerId) }} + {% if defined(groupId) or defined(workspaceId) or defined(partnerId) or defined(programId) %} + AND link_id in (SELECT link_id from workspace_links) + {% end %} + )
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (17)
packages/tinybird/pipes/v2_browsers.pipe
(5 hunks)packages/tinybird/pipes/v2_cities.pipe
(5 hunks)packages/tinybird/pipes/v2_continents.pipe
(5 hunks)packages/tinybird/pipes/v2_count.pipe
(5 hunks)packages/tinybird/pipes/v2_countries.pipe
(5 hunks)packages/tinybird/pipes/v2_devices.pipe
(5 hunks)packages/tinybird/pipes/v2_events.pipe
(5 hunks)packages/tinybird/pipes/v2_os.pipe
(5 hunks)packages/tinybird/pipes/v2_referer_urls.pipe
(5 hunks)packages/tinybird/pipes/v2_referers.pipe
(5 hunks)packages/tinybird/pipes/v2_regions.pipe
(5 hunks)packages/tinybird/pipes/v2_timeseries.pipe
(5 hunks)packages/tinybird/pipes/v2_top_links.pipe
(5 hunks)packages/tinybird/pipes/v2_top_tags.pipe
(2 hunks)packages/tinybird/pipes/v2_top_urls.pipe
(5 hunks)packages/tinybird/pipes/v2_triggers.pipe
(5 hunks)packages/tinybird/pipes/v2_utms.pipe
(5 hunks)
🚧 Files skipped from review as they are similar to previous changes (4)
- packages/tinybird/pipes/v2_referer_urls.pipe
- packages/tinybird/pipes/v2_os.pipe
- packages/tinybird/pipes/v2_browsers.pipe
- packages/tinybird/pipes/v2_cities.pipe
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
- GitHub Check: Vade Review
- GitHub Check: build
🔇 Additional comments (27)
packages/tinybird/pipes/v2_top_tags.pipe (1)
18-18
: Group filter on workspace_links_with_tags — LGTMConditionally filtering by partner_group_id when groupId is defined matches the pattern used elsewhere.
packages/tinybird/pipes/v2_events.pipe (1)
21-21
: GroupId propagation and PREWHERE gating — LGTMConsistent with other v2 pipes; groupId now flows through workspace_links and event predicates.
Also applies to: 56-58, 107-109, 184-186, 263-265
packages/tinybird/pipes/v2_continents.pipe (1)
21-21
: Add groupId filtering and gating — LGTMWorkspace filter and PREWHERE gates now honor groupId across clicks/leads/sales paths.
Also applies to: 52-54, 109-111, 161-163, 210-212
packages/tinybird/pipes/v2_top_links.pipe (2)
21-21
: GroupId support across nodes — LGTMOptional partner_group_id filter and PREWHERE gates updated consistently.
Also applies to: 52-54, 101-103, 145-147, 187-188
21-21
: Run corrected repo-wide check for missing groupId guardsPrevious verification run failed with "/bin/bash: line 6: !: command not found" and a PCRE2 compile error. Re-run the corrected script below and paste the output.
#!/usr/bin/env bash set -euo pipefail echo "Pipes with workspace_links but missing partner_group_id = {{ groupId }}:" rg -l "NODE workspace_links" packages/tinybird/pipes -g "**/*.pipe" | while IFS= read -r f; do if rg -q 'partner_group_id\s*=\s*\{\{\s*groupId\s*\}\}' "$f"; then continue fi echo " - $f" done echo echo "PREWHERE gates missing groupId in OR-chain:" mapfile -t files < <(rg -l 'PREWHERE link_id in \(SELECT link_id from workspace_links' packages/tinybird/pipes -g "**/*.pipe" || true) for f in "${files[@]:-}"; do contexts=$(rg -n -C5 'PREWHERE link_id in \(SELECT link_id from workspace_links' "$f" || true) [ -z "$contexts" ] && continue if echo "$contexts" | rg -q 'defined\(workspaceId\)' && echo "$contexts" | rg -q 'defined\(partnerId\)' && echo "$contexts" | rg -q 'defined\(programId\)'; then if echo "$contexts" | rg -q 'defined\(groupId\)'; then : else echo " - $f" fi fi donepackages/tinybird/pipes/v2_countries.pipe (1)
21-21
: GroupId filters and PREWHERE gates — LGTMMatches the established pattern; behavior unchanged when groupId is absent.
Also applies to: 51-53, 97-99, 149-151, 198-200
packages/tinybird/pipes/v2_top_urls.pipe (1)
21-21
: GroupId-aware filtering for top URLs — LGTMWorkspace constraint and all PREWHERE gates now consider groupId.
Also applies to: 43-45, 81-83, 125-127, 167-169
packages/tinybird/pipes/v2_triggers.pipe (1)
21-21
: Trigger analytics: groupId support — LGTMConsistent gating and workspace filter additions.
Also applies to: 52-54, 100-102, 146-148, 187-189
packages/tinybird/pipes/v2_count.pipe (3)
98-100
: Group PREWHERE gating for leads: LGTM.
137-139
: Group PREWHERE gating for sales: LGTM.
193-195
: Group PREWHERE gating for sales_with_type: LGTM.packages/tinybird/pipes/v2_regions.pipe (4)
48-50
: Group PREWHERE gating in regions_clicks: LGTM.
93-95
: Group PREWHERE gating in regions_leads: LGTM.
140-142
: Group PREWHERE gating in regions_sales: LGTM.
182-184
: Group PREWHERE gating in regions_sales_with_type: LGTM.packages/tinybird/pipes/v2_utms.pipe (3)
125-127
: Group PREWHERE gating in utms_leads: LGTM.
192-194
: Group PREWHERE gating in utms_sales: LGTM.
260-262
: Group PREWHERE gating in utms_sales_with_type: LGTM.packages/tinybird/pipes/v2_timeseries.pipe (5)
94-94
: Add groupId filter in workspace_links — LGTMConsistent with existing partnerId/programId filters; unblocks group-scoped analytics.
208-210
: Leads PREWHERE gate includes groupId — LGTMParity with clicks/sales maintained.
274-276
: Sales PREWHERE gate includes groupId — LGTMKeeps filtering behavior consistent across metrics.
326-329
: Sales-with-type PREWHERE gate includes groupId — LGTMInner CTE respects group scoping as expected.
80-110
: Repo-wide sanity check for PREWHERE gates — no missing groupId foundScanned packages/tinybird/pipes: no PREWHERE gates found that combine not defined(linkId) with workspaceId/partnerId/programId while lacking defined(groupId).
packages/tinybird/pipes/v2_referers.pipe (4)
21-21
: Add groupId filter in workspace_links — LGTMMatches the pattern used elsewhere; enables partner_group_id scoping.
101-103
: Leads PREWHERE gate includes groupId — LGTM
145-147
: Sales PREWHERE gate includes groupId — LGTM
186-189
: Sales-with-type PREWHERE gate includes groupId — LGTM
Summary by CodeRabbit
New Features
Improvements
Chores