Relevant source files
The following files were used as context for generating this wiki page:
- apps/web/lib/analytics/get-analytics.ts
- apps/web/lib/postback/record-postback-event.ts
- apps/web/app/ee/api/track/visit/route.ts
- apps/web/lib/tinybird/record-click.ts
- apps/web/app/api/analytics/route.ts
- apps/web/app/ee/api/track/application/route.ts
- apps/web/lib/tinybird/record-webhook-event.ts
- apps/web/scripts/tinybird/get-sale-events.ts
- apps/web/lib/postback/get-postback-events.ts
- apps/web/app/api/og/analytics/route.tsx
- apps/web/lib/tinybird/get-customer-events-tb.ts
- apps/web/app/ee/api/cron/streams/update-click-stats/route.ts
- apps/web/lib/tinybird/record-lead.ts
- apps/web/lib/tinybird/get-lead-events.ts
- apps/web/lib/tinybird/record-click-zod.ts
- apps/web/lib/tinybird/record-sale.ts
- apps/web/lib/tinybird/record-link.ts
- apps/web/lib/dub.ts
- apps/web/lib/analytics/get-events.ts
- apps/web/lib/tinybird/client.ts
- apps/web/lib/tinybird/log-import-error.ts
- apps/web/scripts/dub-wrapped.ts
- apps/web/lib/tinybird/index.ts
- apps/web/scripts/tinybird/delete-links.ts
- apps/web/lib/tinybird/get-click-event.ts
- apps/web/ui/analytics/events/example-data.ts
- apps/web/lib/tinybird/get-webhook-events.ts
- apps/web/lib/tinybird/get-top-links-by-countries.ts
- apps/web/lib/upstash/redis-streams/workspace-click-events.ts
- apps/web/scripts/tinybird/update-click-event.ts
Overview
Dub leverages Tinybird as its underlying real-time analytics and event streaming backbone to ingest, process, and query millions of click, lead, sale, webhook, and visit telemetry events. By utilizing the @chronark/zod-bird client library, the system coordinates low-latency data pipelines that feed high-performance parameterized pipe endpoints. These endpoints power dynamic analytical reporting, workspace metrics aggregations, and raw event lookups across distributed workloads.
Sources: apps/web/lib/tinybird/client.ts:1-6, apps/web/lib/tinybird/record-click-zod.ts:1-42
Tinybird Client Setup and Ingestion Endpoints
Client Instantiation and Configuration
The Tinybird client is initialized using the @chronark/zod-bird package. The client instance tb requires two environment variables for authentication and connection targeting: TINYBIRD_API_KEY for authorization and TINYBIRD_API_URL for the API base URL.
import { Tinybird } from "@chronark/zod-bird";
export const tb = new Tinybird({
token: process.env.TINYBIRD_API_KEY as string,
baseUrl: process.env.TINYBIRD_API_URL as string,
});Sources: apps/web/lib/tinybird/client.ts:1-6
Note
The tb client export serves as the foundation for building ingest endpoints and pipes across all data streams, including click tracking, lead submissions, and link synchronizations.
Sources: apps/web/lib/tinybird/client.ts:3-5, apps/web/lib/tinybird/index.ts:1-8
Ingestion Endpoints and Schemas
Ingestion pipelines define schemas using zod/v4 to validate event payloads before pushing them to Tinybird data sources. For example, the click recording endpoint validates input objects against recordClickZodSchema and targets the dub_click_events datasource with wait: true.
export const recordClickZod = tb.buildIngestEndpoint({
datasource: "dub_click_events",
event: recordClickZodSchema,
wait: true,
});Sources: apps/web/lib/tinybird/record-click-zod.ts:39-42
| Endpoint Constant | Tinybird Datasource | Description |
|---|---|---|
recordClickZod | dub_click_events | Ingests validated click events with wait: true |
recordPostbackEvent | dub_postback_events | Ingests postback event telemetry |
recordWebhookEvent | dub_webhook_events | Ingests webhook delivery event logs |
recordLead / recordLeadWithTimestamp | dub_lead_events | Ingests conversion lead events |
recordSale / recordSaleWithTimestamp | dub_sale_events | Ingests conversion sale events |
logImportError | dub_import_error_logs | Logs data import errors |
Sources: apps/web/lib/postback/record-postback-event.ts:4-7, apps/web/lib/tinybird/record-webhook-event.ts:4-7, apps/web/lib/tinybird/record-lead.ts:5-14, apps/web/lib/tinybird/record-sale.ts:5-14, apps/web/lib/tinybird/log-import-error.ts:4-7
Click and Interaction Ingestion Pipeline
Request Validation and Filtering
Incoming requests to record a click pass through recordClick, which verifies the presence of a clickId and checks request headers or query parameters for dub-no-track. Except when the trigger is set to "deeplink", bot detection executes via detectBot(req); requests identified as bots are immediately discarded.
export async function recordClick({
req,
clickId,
workspaceId,
linkId,
domain,
key,
url,
programId,
partnerId,
skipRatelimit,
timestamp,
referrer,
trigger = "link",
shouldCacheClickId,
}: {
req: Request;
clickId?: string;
linkId: string;
workspaceId?: string;
domain: string;
key: string;
url?: string;
programId?: string;
partnerId?: string;
skipRatelimit?: boolean;
timestamp?: string;
referrer?: string;
trigger?: string;
shouldCacheClickId?: boolean;
}) {
if (!clickId) {
return null;
}
const searchParams = new URL(req.url).searchParams;
if (req.headers.has("dub-no-track") || searchParams.has("dub-no-track")) {
return null;
}
const ua = userAgent(req);
if (trigger !== "deeplink") {
const isBot = detectBot(req);
if (isBot) {
console.log(`Click not recorded ❌ – Bot detected.`, {
ua,
isBot,
});
return null;
}
}Sources: apps/web/lib/tinybird/record-click.ts:22-78
Warning
Bot checks are deliberately bypassed when trigger evaluates to "deeplink", allowing deep link interactions to bypass standard client filtering rules.
Metadata Enrichment and Deduplication
After passing initial filters, the pipeline computes an identity hash. Unless skipRatelimit is enabled, it queries recordClickCache to prevent duplicate clicks from the same IP address and domain/key pair within a one-hour window. QR code detection runs via detectQr(req), shifting the trigger type to "qr" if matched. Geolocation extracts continent and country-region headers on Vercel deployments, while falling back to LOCALHOST_GEO_DATA and LOCALHOST_IP locally. IP addresses belonging to EU country codes (EU_COUNTRY_CODES) are scrubbed to preserve privacy compliance.
const identityHash = await getIdentityHash(req);
if (!skipRatelimit) {
try {
const cachedClickId = await recordClickCache.get({
domain,
key,
identityHash,
});
if (cachedClickId) {
return null;
}
} catch (error) {
console.error(`[recordClickCache error]: ${error}`);
return null;
}
}
const isQr = detectQr(req);
if (isQr) {
trigger = "qr";
}
const { continent, region } =
process.env.VERCEL === "1"
? {
continent: req.headers.get("x-vercel-ip-continent"),
region: req.headers.get("x-vercel-ip-country-region"),
}
: LOCALHOST_GEO_DATA;
const geo =
process.env.VERCEL === "1" ? geolocation(req) : LOCALHOST_GEO_DATA;
const ip = process.env.VERCEL === "1" ? ipAddress(req) : LOCALHOST_IP;
const isEuCountry = geo.country && EU_COUNTRY_CODES.includes(geo.country);Sources: apps/web/lib/tinybird/record-click.ts:80-123
Dual Redis Stream Fanout and Tinybird Ingestion
Asynchronously via waitUntil, the constructed click payload is sent to Tinybird's event ingestion endpoint (dub_click_events), recorded in the rate limit cache for one hour, and fanned out concurrently to two distinct Upstash Redis streams: publishLinkClickEvent and publishWorkspaceClickEvent.
fetchWithRetry(
`${process.env.TINYBIRD_API_URL}/v0/events?name=dub_click_events&wait=true`,
{
method: "POST",
headers: {
Authorization: `Bearer ${process.env.TINYBIRD_API_KEY}`,
},
body: JSON.stringify(clickData),
},
).then((res) => res.json()),
recordClickCache.set({
domain,
key,
identityHash,
clickId,
}),
publishLinkClickEvent({
linkId,
timestamp: clickData.timestamp,
...(workspaceId && url && { workspaceId }),
...(programId && partnerId && { programId, partnerId }),
}),
publishWorkspaceClickEvent(clickData),Sources: apps/web/lib/tinybird/record-click.ts:172-199
Workspace click events undergo schema validation via clickEventSchemaTB, converting boolean flags qr and bot into numeric representations before pushing to the workspace:click:events stream if the workspace has active link-click webhooks configured.
export const workspaceClickEventStream = new RedisStream(STREAM_KEY);
export const publishWorkspaceClickEvent = async (event) => {
try {
const parsedEvent = clickEventSchemaTB.parse({
...event,
qr: Number(event.qr),
bot: Number(event.bot),
});
const hasLinkClickedWebhooks = await clickWebhookWorkspaces.has(
parsedEvent.workspace_id,
);
if (!hasLinkClickedWebhooks) {
return;
}
return await redis.xadd(STREAM_KEY, "*", parsedEvent);
} catch (error) {
logger.error("stream.publish_failed", {
service: "upstash",
streamKey: STREAM_KEY,
error: toErrorFields(error),
correlation: {
workspaceId: event.workspace_id,
clickId: event.click_id,
linkId: event.link_id,
},
});
await logger.flush();
throw error;
}
};Sources: apps/web/lib/upstash/redis-streams/workspace-click-events.ts:9-44
Note
If shouldCacheClickId is enabled during ingestion, the click data is explicitly stored under clickIdCache:${clickId} in Redis with a 5-minute expiration to bridge the latency gap before ingested rows become available directly in Tinybird queries.
Link Metadata Synchronization
Payload Transformation and Ingestion
The link metadata synchronization subsystem records and updates link dimensions, casing normalization, and domain parameters into Tinybird datasources. Link synchronization is handled by the recordLink utility function, which transforms expanded link objects and pushes them into the dub_links_metadata Tinybird datasource.
Before records are dispatched to Tinybird, the transformLinkTB function maps an ExpandedLink entity into the schema shape required by dub_links_metadata. It extracts tag associations into flat string arrays, maps program enrollments to group identifiers and partner tag collections, and binds project identifiers to workspace_id.
const transformLinkTB = (link: ExpandedLink) => {
const key = decodeKeyIfCaseSensitive({
domain: link.domain,
key: link.key,
});
return {
link_id: link.id,
domain: link.domain,
key,
url: link.url,
tag_ids: link.tags?.map(({ tag }) => tag.id) ?? [],
folder_id: link.folderId ?? "",
tenant_id: link.tenantId ?? "",
program_id: link.programId ?? "",
partner_id: link.partnerId ?? "",
partner_group_id: link.programEnrollment?.groupId ?? "",
partner_tag_ids:
link.programEnrollment?.programPartnerTags?.map(
({ partnerTagId }) => partnerTagId,
) ?? [],
workspace_id: link.projectId,
created_at: link.createdAt,
};
};Sources: apps/web/lib/tinybird/record-link.ts:52-76
The top-level recordLink function accepts either a single ExpandedLink or an array of links. When an array is provided, it maps each item through transformLinkTB and appends the deleted flag before invoking the ingestion endpoint with wait: true.
export const recordLink = async (
payload: ExpandedLink | ExpandedLink[],
{ deleted }: { deleted?: boolean } = {},
) => {
if (Array.isArray(payload)) {
return await recordLinkTB(
payload.map(transformLinkTB).map((p) => ({ ...p, deleted })),
);
} else {
return await recordLinkTB({ ...transformLinkTB(payload), deleted });
}
};Sources: apps/web/lib/tinybird/record-link.ts:78-89
Note
The key field undergoes case-sensitivity checks via decodeKeyIfCaseSensitive during transformation to preserve correct link routing rules across case-sensitive domains.
Domain Transfer Deletion Scripts
Administrative maintenance tasks, such as handling domain transfers or purging metadata associated with specific domains, invoke Tinybird REST deletion endpoints via authenticated HTTP POST requests. The deletion script targets multiple datasources simultaneously using Promise.allSettled.
| Target Tinybird Datasource | Deletion Condition Source | HTTP Method & Auth |
|---|---|---|
dub_links_metadata | domain = 'xyz' | POST, Bearer Token (TINYBIRD_API_KEY) |
dub_links_metadata_latest | domain = 'xyz' | POST, Bearer Token (TINYBIRD_API_KEY) |
dub_regular_links_metadata_latest | domain = 'xyz' | POST, Bearer Token (TINYBIRD_API_KEY) |
const deleteCondition = "domain = 'xyz'";
// delete data from tinybird
const deleteRes = await Promise.allSettled([
deleteData({
dataSource: "dub_links_metadata",
deleteCondition,
}),
deleteData({
dataSource: "dub_links_metadata_latest",
deleteCondition,
}),
deleteData({
dataSource: "dub_regular_links_metadata_latest",
deleteCondition,
}),
]);
console.log(deleteRes);
}Sources: apps/web/scripts/tinybird/delete-links.ts:5-23
The underlying deleteData utility constructs URL-encoded bodies containing delete_condition parameters and communicates with Tinybird's US-East API region.
const deleteData = async ({
dataSource,
deleteCondition,
}: {
dataSource: string;
deleteCondition: string;
}) => {
return fetch(
`https://api.us-east.tinybird.co/v0/datasources/${dataSource}/delete`,
{
method: "POST",
headers: {
Authorization: `Bearer ${process.env.TINYBIRD_API_KEY}`,
"Content-Type": "application/x-www-form-urlencoded",
},
body: `delete_condition=${deleteCondition}`,
},
).then((res) => res.json());
};Sources: apps/web/scripts/tinybird/delete-links.ts:25-43
Caution
Deletion operations executed against Tinybird datasources via delete_condition permanently remove matching rows across the specified metadata views and cannot be undone.
Analytics Pipe Query Processing
Parameterized Analytics Pipelines
Query execution for timeseries metrics, aggregated link counts, and country statistics relies on parameterized Tinybird pipe endpoints built dynamically via @chronark/zod-bird. The getAnalytics function inspects input criteria such as grouping parameters and event types to select the matching version 4 pipe (v4_count, v4_timeseries, v4_group_by_link_metadata, or v4_group_by). Pipe inputs are validated against analyticsFilterTB parameters, returning structured analytics records containing dynamic groupBy fields, clicks, leads, sales, and sale amounts.
const pipe = tb.buildPipe({
pipe: ["count", "timeseries"].includes(groupBy!)
? `v4_${groupBy}`
: [
"top_folders",
"top_link_tags",
"top_domains",
"top_partners",
"top_partner_tags",
"top_groups",
].includes(groupBy!)
? "v4_group_by_link_metadata"
: "v4_group_by",
parameters: analyticsFilterTB,
data: z.object({
groupByField: z.string(),
clicks: z.number().default(0),
leads: z.number().default(0),
sales: z.number().default(0),
saleAmount: z.number().default(0),
// only for cities and regions groupBy
country: z.string().optional(),
region: z.string().optional(),
}),
});Sources: apps/web/lib/analytics/get-analytics.ts:98-123
When querying country metrics for specific link sets, dedicated helper pipes such as v3_group_by_link_country process array filters for linkIds alongside formatted date ranges to return granular click distributions per country code.
const getTopLinksByCountriesTB = tb.buildPipe({
pipe: "v3_group_by_link_country",
parameters: inputSchema,
data: responseSchema,
});Sources: apps/web/lib/tinybird/get-top-links-by-countries.ts:18-22
Note
Queries requesting all-time click metrics with a count interval and no supplemental dimensional filters bypass Tinybird pipes entirely, querying relational tables directly via PlanetScale SQL execution.
Filter Assembly and Transformation
Parameters collected from API requests are transformed into serialized query requirements before hitting Tinybird endpoints. Advanced filter predicates, metadata query strings, and date boundaries formatted for ClickHouse are bundled into the tinybirdParams payload alongside explicit operator flags.
| Tinybird Parameter | Source Property | Description |
|---|---|---|
workspaceId | params.workspaceId | Target workspace identifier for isolation |
groupBy | groupBy | Dimension used to aggregate click and conversion metrics |
eventType | event | Metric category (clicks, leads, sales, saleAmount, composite) |
start | startDate | Start timestamp formatted via formatUTCDateTimeClickhouse |
end | endDate | End timestamp formatted via formatUTCDateTimeClickhouse |
granularity | granularity | Time bucket resolution (minute, hour, day, month) |
timezone | timezone | Target timezone string defaulting to "UTC" |
filters | allFilters | Serialized JSON array combining metadata and advanced criteria |
const tinybirdParams: any = {
workspaceId,
customerId: params.customerId,
programId: params.programId,
partnerId: partnerIdParam,
partnerIdOperator,
partnerTagId: partnerTagIdParam,
partnerTagIdOperator,
tenantId: tenantIdParam,
tenantIdOperator,
groupId: groupIdParam,
groupIdOperator,
linkId: linkIdParam,
linkIdOperator,
domain: domainParam,
domainOperator,
folderId: folderIdParam,
folderIdOperator,
tagId: tagIdParam,
tagIdOperator,
groupBy,
eventType: event,
start: formatUTCDateTimeClickhouse(startDate),
end: formatUTCDateTimeClickhouse(endDate),
granularity,
timezone,
region: typeof regionForPipe === "string" ? regionForPipe : undefined,
root: params.root,
saleType: params.saleType,
filters: allFilters.length > 0 ? JSON.stringify(allFilters) : undefined,
};Sources: apps/web/lib/analytics/get-analytics.ts:164-194
Tip
When executing batch analytical scripts such as dub-wrapped.ts, parallelized helper calls to getAnalytics aggregate top links and country breakdowns simultaneously across year-to-date intervals without blocking execution flow.
const payloads = await Promise.all(
data.map(async ({ projectId, _count, _sum }) => {
const [topLinks, topCountries] = (await Promise.all([
getAnalytics({
workspaceId: projectId!,
event: "clicks",
groupBy: "top_links",
interval: "ytd",
root: false,
}),
getAnalytics({
workspaceId: projectId!,
event: "clicks",
groupBy: "countries",
interval: "ytd",
}),
])) as [AnalyticsTopLinks[], AnalyticsCountries[]];
return {
year: 2024,
workspaceId: projectId!,
totalLinks: _count.id,
totalClicks: _sum.clicks ?? 0,
topLinks: topLinks.slice(0, 5).map(({ domain, key, clicks }) => ({
item: `${domain}/${key}`,
count: clicks,
})),
topCountries: topCountries.slice(0, 5).map(({ country, clicks }) => ({
item: country,
count: clicks,
})),
};
}),
);Sources: apps/web/scripts/dub-wrapped.ts:56-89
Raw Event Queries and Lookups
Event Retrieval Pipes and Functions
Dub queries raw, individual records by leveraging specialized Tinybird pipe wrappers and direct data source operations. Individual click records are fetched via getClickEvent, which checks a Redis cache first under clickIdCache:${clickId} before falling back to the get_click_event Tinybird pipe. Conversion and telemetry events use similar parameterized pipes: getLeadEvents queries lead records filtered by an array of customer identifiers, getSaleEvents targets internal sale records for a specific customer ID, getPostbackEvents retrieves postback logs by postback ID, and getWebhookEvents queries webhook dispatch history by webhook identifier.
const getClickEventTB = tb.buildPipe({
pipe: "get_click_event",
parameters: z.object({
clickId: z.string(),
}),
data: clickEventSchemaTB,
});Sources: apps/web/lib/tinybird/get-click-event.ts:7-13
| Pipe Function | Tinybird Pipe Name | Parameter Schema | Data Output Schema |
|---|---|---|---|
getClickEventTB | get_click_event | clickId: z.string() | clickEventSchemaTB |
getLeadEvents | get_lead_events | customerIds: z.string().array() | leadEventSchemaTB |
getSaleEvents | internal_get_sale_events | customerId: z.string() | z.any() |
getPostbackEvents | get_postback_events | postbackId: z.string() | postbackEventOutputSchemaTB |
getWebhookEvents | get_webhook_events | webhookId: z.string() | webhookEventSchemaTB |
getCustomerEventsTB | v2_customer_events | z.any() | z.any() |
Sources: apps/web/scripts/tinybird/get-sale-events.ts:4-10, apps/web/lib/postback/get-postback-events.ts:5-11, apps/web/lib/tinybird/get-customer-events-tb.ts:4-8, apps/web/lib/tinybird/get-lead-events.ts:5-10, apps/web/lib/tinybird/get-click-event.ts:7-13, apps/web/lib/tinybird/get-webhook-events.ts:5-11
Note
The getCustomerEventsTB wrapper delegates directly to the v2_customer_events pipe, dynamically appending linkIds and limit parameters when provided alongside the required customerId.
export const getCustomerEventsTB = async ({
customerId,
linkIds,
limit,
}: {
customerId: string;
linkIds?: string[];
limit?: number;
}) => {
return await pipe({
customerId,
...(linkIds ? { linkIds } : {}),
...(limit ? { limit } : {}),
});
};Sources: apps/web/lib/tinybird/get-customer-events-tb.ts:10-23
Unified Event Feeds and Modification Scripts
The getEvents function queries the v4_events pipe to unify retrieval across click, lead, and sale event types. It builds complex criteria arrays by parsing metadata queries and advanced filters, mapping workspace link filters with operators, and resolving relational data from Prisma for associated links and customers.
const pipe = tb.buildPipe({
pipe: "v4_events",
parameters: eventsFilterTB,
data:
{
clicks: clickEventSchemaTBEndpoint,
leads: leadEventSchemaTBEndpoint,
sales: saleEventSchemaTBEndpoint,
}[eventType] ?? clickEventSchemaTBEndpoint,
});Sources: apps/web/lib/analytics/get-events.ts:77-86
Caution
When updating raw click event properties via administrative scripts, modifications require pushing the amended JSON payload to the dub_click_events data source with wait=true while concurrently executing targeted deletions against both dub_click_events_mv and dub_click_events_id datasources using explicit delete_condition parameters.
const deleteData = async ({
dataSource,
clickId,
columnName,
oldValue,
}: {
dataSource: string;
clickId: string;
columnName: string;
oldValue: string;
}) => {
return fetch(
`https://api.us-east.tinybird.co/v0/datasources/${dataSource}/delete`,
{
method: "POST",
headers: {
Authorization: `Bearer ${process.env.TINYBIRD_API_KEY}`,
"Content-Type": "application/x-www-form-urlencoded",
},
body: `delete_condition=click_id='${clickId}' and ${columnName}='${oldValue}'`,
},
).then((res) => res.json());
};Sources: apps/web/scripts/tinybird/update-click-event.ts:53-75
Stream Consumption and Aggregation Cron
Batch Processing and Aggregation Constants
The background cron route defined in apps/web/app/(ee)/api/cron/streams/update-click-stats/route.ts manages stream ingestion from Upstash Redis streams into relational storage. Constants govern batch sizes, concurrency thresholds, and distributed execution protection.
| Constant Name | Value | Purpose |
|---|---|---|
BATCH_SIZE | 10_000 | Maximum stream entries to consume per cron run |
SUB_BATCH_SIZE | 50 | Database updates to run in parallel within each batch |
BACKLOG_ALERT_THRESHOLD | 50_000 | Alert threshold when stream length exceeds this count |
BACKLOG_AGE_ALERT_MS | 300_000 (5 mins) | Alert threshold when oldest pending entry exceeds age |
LOCK_KEY | lock:update-click-stats | Redis key preventing concurrent GET/POST double-counting |
LOCK_TTL_SECONDS | 600 | Lock TTL matching cron maxDuration to outlive execution |
Sources: apps/web/app/ee/api/cron/streams/update-click-stats/route.ts:15-20
Warning
The distributed LOCK_KEY uses a TTL of 600 seconds to match the maximum cron duration, ensuring that overlapping HTTP requests do not invoke duplicate batch processing cycles that would double-count click stats.
Sources: apps/web/app/ee/api/cron/streams/update-click-stats/route.ts:19-20
Aggregation and Sub-Batching Architecture
The aggregateClickStats function iterates through incoming RedisStreamEntry<LinkClickEvent>[] objects, isolating metadata to construct grouped maps for links, workspaces, and partner program enrollments. Timestamps are parsed via Date.parse, defaulting to Date.now() if the value is invalid or non-finite.
const aggregateClickStats = (
entries: RedisStreamEntry<LinkClickEvent>[],
): {
linkUpdates: LinkAggregate[];
workspaceUpdates: WorkspaceAggregate[];
enrollmentUpdates: EnrollmentAggregate[];
} => {
const links = new Map<string, LinkAggregate>();
const workspaces = new Map<string, WorkspaceAggregate>();
const enrollments = new Map<string, EnrollmentAggregate>();
for (const entry of entries) {
const { linkId, workspaceId, programId, partnerId, timestamp } = entry.data;
if (!linkId) {
continue;
}
const parsedTimestamp = Date.parse(timestamp);
const lastClicked = Number.isFinite(parsedTimestamp)
? parsedTimestamp
: Date.now();
const existingLink = links.get(linkId);
if (existingLink) {
existingLink.clicks += 1;
existingLink.lastClicked = Math.max(
existingLink.lastClicked,
lastClicked,
);
existingLink.entryIds.push(entry.id);
} else {
links.set(linkId, {
linkId,
clicks: 1,
lastClicked,
entryIds: [entry.id],
});
}
if (workspaceId) {
const existingWorkspace = workspaces.get(workspaceId);
if (existingWorkspace) {
existingWorkspace.clicks += 1;
} else {
workspaces.set(workspaceId, {
workspaceId,
clicks: 1,
});
}
}
if (programId && partnerId) {
const key = `${programId}:${partnerId}`;
const existingEnrollment = enrollments.get(key);
if (existingEnrollment) {
existingEnrollment.clicks += 1;
} else {
enrollments.set(key, {
programId,
partnerId,
clicks: 1,
});
}
}
}
return {
linkUpdates: Array.from(links.values()),
workspaceUpdates: Array.from(workspaces.values()),
enrollmentUpdates: Array.from(enrollments.values()),
};
};Sources: apps/web/app/ee/api/cron/streams/update-click-stats/route.ts:40-112
Tip
The processInSubBatches utility slices accumulated update arrays into chunks defined by SUB_BATCH_SIZE and evaluates them using Promise.allSettled, tracking successes and isolating errors into a returned failure collection without aborting the broader cron job.
const processInSubBatches = async <T>(
items: T[],
handler: (item: T) => Promise<{ success: boolean; error?: unknown }>,
) => {
const errors: unknown[] = [];
let totalProcessed = 0;
for (let i = 0; i < items.length; i += SUB_BATCH_SIZE) {
const batch = items.slice(i, i + SUB_BATCH_SIZE);
const results = await Promise.allSettled(batch.map(handler));
for (const result of results) {
if (result.status === "fulfilled" && result.value.success) {
totalProcessed++;
} else if (result.status === "fulfilled" && result.value.error) {
errors.push(result.value.error);
} else if (result.status === "rejected") {
errors.push(result.reason);
}
}
}
return { totalProcessed, errors };
};Sources: apps/web/app/ee/api/cron/streams/update-click-stats/route.ts:114-137
Related
On This Page
OverviewTinybird Client Setup and Ingestion EndpointsClient Instantiation and ConfigurationIngestion Endpoints and SchemasClick and Interaction Ingestion PipelineRequest Validation and FilteringMetadata Enrichment and DeduplicationDual Redis Stream Fanout and Tinybird IngestionLink Metadata SynchronizationPayload Transformation and IngestionDomain Transfer Deletion ScriptsAnalytics Pipe Query ProcessingParameterized Analytics PipelinesFilter Assembly and TransformationRaw Event Queries and LookupsEvent Retrieval Pipes and FunctionsUnified Event Feeds and Modification ScriptsStream Consumption and Aggregation CronBatch Processing and Aggregation ConstantsAggregation and Sub-Batching ArchitectureRelated