Getting Started
Core Architecture
Link Engine
Analytics & Attribution
Partners & Affiliates
Third-Party Integrations
Identity & Security
Automation & Messaging
Developer Tools
The following files were used as context for generating this wiki page:
Google Ads Attribution integrates Dub with Google Ads to bridge referral tracking and campaign optimization by automatically uploading offline click conversions. It solves the fragmentation between click acquisition and downstream conversion data, empowering workspaces to attribute leads and sales back to the specific Google Ads clicks that drove them. The system manages secure OAuth credential lifecycles, maps internal workspace event names to Google Ads conversion actions, extracts and propagates GCLID identifiers across the tracking layer, and executes background offline upload pipelines. By connecting conversion ingestion workflows with automated Google Ads reporting, this integration enables precise ROI measurement and campaign performance tuning.
Sources: apps/web/scripts/create-integration.ts:13-22, apps/web/lib/integrations/google-ads/upload-conversion.ts:105-223, apps/web/lib/integrations/google-ads/oauth.ts:11-220
The Google Ads OAuth subsystem oversees the complete installation lifecycle, secure token exchange, encrypted credential persistence, and concurrent token refreshing using distributed locking via Upstash Redis. When a user initiates the installation process, the application constructs an authorization request URL with offline access and consent prompts, storing the request state in Redis with a 30-minute expiration. Upon callback completion, the route validates session ownership, enforces workspace owner permissions and plan capabilities, and persists encrypted tokens alongside inferred customer settings.
Sources: apps/web/app/ee/api/google-ads/callback/route.ts:30-135, apps/web/lib/integrations/google-ads/oauth.ts:21-38
When a user completes authorization, Google redirects to the callback route where a sequence of validation, token processing, and database installation steps execute.
GET(req) → googleAdsOAuthProvider.exchangeCodeForToken() → prisma.project.findUniqueOrThrow() → googleAdsAuthTokenSchema.parse() → GoogleAdsApi.listAccessibleCustomers() → prisma.installedIntegration.findFirst() → googleAdsSettingsSchema.parse() → installIntegration() → waitUntil(googleAdsInstalledWorkspaces.add()) → redirect()
GET(req): Entry point handling the incoming OAuth redirect request.googleAdsOAuthProvider.exchangeCodeForToken(): Exchanges the authorization code for an OAuth token payload.prisma.project.findUniqueOrThrow(): Retrieves workspace metadata and verifying user membership roles.googleAdsAuthTokenSchema.parse(): Validates token structures and encrypts both access_token and refresh_token fields.GoogleAdsApi.listAccessibleCustomers(): Queries accessible accounts to infer login customer identifiers.prisma.installedIntegration.findFirst(): Checks for existing integration installation records.googleAdsSettingsSchema.parse(): Parses combined existing and new customer settings.installIntegration(): Persists the encrypted credentials and workspace settings to the database.waitUntil(googleAdsInstalledWorkspaces.add()): Registers the newly installed workspace in the background.redirect(): Navigates the user back to the workspace Google Ads settings page.When an access token expires or nears expiration, getAccessToken() evaluates token validity using a 60-second buffer. If expired, it attempts to acquire a Redis-backed lock before hitting the Google OAuth token endpoint.
Warning
If a refresh lock cannot be acquired on the first attempt, the process does not fail immediately. Instead, it enters a polling loop using waitForRefreshedCredentials() that waits between 200ms and 400ms per iteration up to a 5-second deadline to catch a token refreshed concurrently by another worker thread.
Sources: apps/web/lib/integrations/google-ads/oauth.ts:40-78, apps/web/lib/integrations/google-ads/oauth.ts:160-177, apps/web/lib/integrations/google-ads/oauth.ts:210-219
The Google Ads OAuth provider is initialized with fixed configuration parameters defining the authorization endpoints, scope, and Redis key prefixes.
The low-level Google Ads API client wrapper manages request signing, customer hierarchy inference, search queries, and conversion uploads. It provides methods for interacting directly with the Google Ads REST endpoints and Data Manager API.
All requests made through googleAdsFetch attach required authorization headers, developer credentials, and optional login customer context. The getGoogleAdsHeaders function constructs these headers from request options.
Warning
If a request returns a non-OK status, googleAdsFetch parses the response text and error details, throwing an error containing the request path, status code, and formatted API error message.
The client wrapper interacts with both the Google Ads API for querying resources and the Data Manager API for event ingestion.
listUploadClickConversionActions(customerId): Executes a search stream query filtering for UPLOAD_CLICKS conversion actions with an ENABLED status, mapping the results through validation schemas.uploadClickConversion(...): Constructs destination and event payloads before submitting offline click conversions to the Data Manager API endpoint (events:ingest).Tip
New integrations must use the Data Manager API (events:ingest) rather than ConversionUploadService.UploadClickConversions when uploading offline click conversions.
The conversion action and event mapping subsystem allows workspaces to bridge internal business telemetry with Google Ads conversion reporting. Through the workspace integration settings interface and accompanying server actions, administrators associate Dub lead and sale event names with verified Google Ads conversion actions.
Sources: apps/web/lib/integrations/google-ads/ui/settings.tsx:72-343, apps/web/lib/integrations/google-ads/update-google-ads-settings.ts:26-141
When an administrator selects a Google Ads customer account within the settings interface, the client fetches available conversion actions by invoking the workspace API route.
The call-chain execution order for listing conversion actions follows this sequence:
GET route handler (apps/web/app/ee/api/google-ads/conversion-actions/route.ts) validates workspace permissions and installed integration status.googleAdsOAuthProvider.getAccessToken() retrieves a valid OAuth token for the installation.getLoginCustomerIdCandidates() computes manager hierarchy options based on customer settings.GoogleAdsApi constructor instantiates the API client with the token, login customer context, and target customer ID.googleAdsApi.listUploadClickConversionActions(customerId) queries the Google Ads API for enabled upload-click conversion actions.Warning
If a candidate login customer ID throws a USER_PERMISSION_DENIED error during conversion action retrieval, the iteration catches the error and tests the next candidate ID in the list before failing.
Once lead and sale event mappings are configured in the settings form, submitting changes triggers the updateGoogleAdsSettingsAction server action. This action enforces strict validation checks on customer association, format prefixes, and mapping uniqueness.
Important
The helper function uniqueMappingEventNames automatically sanitizes event name arrays by passing them through new Set() to remove duplicate entries prior to persisting settings in PostgreSQL.
The tracking layer extracts and propagates tracking identifiers and request metadata during click and visit ingestion. Incoming requests to tracking routes capture query parameters, perform identity hashing, verify workspace allowed hostnames, and persist rich telemetry records to Tinybird and Upstash Redis.
Sources: apps/web/app/ee/api/track/click/route.ts:25-158, apps/web/app/ee/api/track/visit/route.ts:18-104, apps/web/lib/tinybird/record-click.ts:22-236
When a click tracking request arrives at the /api/track/click endpoint, it parses domain and key arguments, validates request parameters via Zod schemas, and executes concurrent cache lookups for click identifiers and link metadata.
The call-chain execution order for recording a click event follows this sequence:
POST route handler (apps/web/app/ee/api/track/click/route.ts) parses the request body using trackClickSchema.getIdentityHash(req) computes a unique visitor identity hash.redisGlobalWithTimeout.mget() checks recordClickCache and linkCache in Upstash Redis.getLinkWithPartner() queries Planetscale if the link is not cached.verifyAnalyticsAllowedHostnames() validates the request origin against workspace allowed hostnames.recordClick() ingests the click event into Tinybird and publishes streams to Upstash.Note
If a cached click identifier exists in Redis for the given identity hash, the endpoint reuses cachedClickId instead of allocating a new nanoid or duplicating the Tinybird ingestion record.
Sources: apps/web/app/ee/api/track/click/route.ts:78-80, apps/web/app/ee/api/track/click/route.ts:114-114
The recordClick function aggregates geographical data, user-agent details, and request headers into a comprehensive click payload before dispatching it to external analytics sinks.
Warning
Requests originating from European Union country codes (EU_COUNTRY_CODES) automatically have their IP address stripped and recorded as an empty string to comply with privacy regulations.
Once the telemetry object is compiled, recordClick caches the click ID in Redis for 5 minutes and dispatches background tasks via waitUntil to ensure non-blocking response delivery.
waitUntil(
(async () => {
const response = await Promise.allSettled([
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),
]);
})(),
);Downstream conversion tracking triggers bridge lead generation and sale ingestion workflows with offline upload pipelines. Whenever a lead or sale occurs via API endpoints, Shopify webhooks, or Stripe event integrations, the underlying application logic packages conversion attributes and invokes queueGoogleAdsConversionUpload inside Vercel's waitUntil asynchronous execution context. This architecture ensures that ingestion routes return HTTP responses immediately while offline conversion payloads are queued for delivery.
Sources: apps/web/lib/api/conversions/track-lead.ts:226-231, apps/web/app/ee/api/stripe/integration/webhook/utils/sync-customer.ts:213-297, apps/web/lib/integrations/shopify/create-lead.ts:139-165, apps/web/lib/api/conversions/track-sale.ts:348-667
Conversion triggers are embedded directly into core domain workflows. For leads, conversion hooks execute after verifying click metadata, recording the event in Tinybird, incrementing link statistics, and updating workspace usage. For sales, conversion hooks execute alongside partner commission creation and revenue metrics updates.
queueGoogleAdsConversionUpload({
workspaceId: workspace.id,
eventType: EventType.lead,
eventId: leadData.event_id,
eventName: leadData.event_name,
conversionDateTime: new Date().toISOString(),
conversionCount: 1,
click: {
id: clickData.click_id,
url: clickData.url,
},
})Sources: apps/web/app/ee/api/stripe/integration/webhook/utils/sync-customer.ts:284-295, apps/web/lib/integrations/shopify/create-lead.ts:153-164, apps/web/lib/api/conversions/track-sale.ts:655-667
Note
Sales upload triggers pass financial metrics including conversionValue (derived from saleData.amount) and currencyCode (derived from saleData.currency), whereas lead triggers pass conversionCount: 1 and omit currency details.
The fields transmitted from ingestion workflows to the conversion upload queue vary depending on whether the event represents a lead or a sale transaction.
Sources: apps/web/app/ee/api/stripe/integration/webhook/utils/sync-customer.ts:284-295, apps/web/lib/integrations/shopify/create-lead.ts:153-164, apps/web/lib/api/conversions/track-sale.ts:655-667
The offline conversion upload pipeline handles queueing conversion payloads, publishing them via QStash, executing background worker tasks, formatting currency values, and logging errors. When a conversion payload is prepared, queueGoogleAdsConversionUpload() first verifies the presence of valid click identifiers (gclid, gbraid, or wbraid) on the click URL using extractGoogleAdsClickId(). It checks if the workspace has installed the Google Ads integration via googleAdsInstalledWorkspaces.has(). If the currency is not a zero-decimal currency, it normalizes major currency units by dividing conversionValue by 100.
Note
Non-zero-decimal currencies such as USD and EUR are divided by 100 before queueing because Google Ads Data Manager expects amounts in major currency units rather than minor currency subunits (cents).
Once validated and normalized, the payload is published to QStash targeting /api/google-ads/upload-conversion. The publish operation specifies a maximum of 3 retries and a deterministic deduplication ID formatted as google-ads-${payload.workspaceId}-${payload.eventId}.
const response = await qstash.publishJSON({
url: `${APP_DOMAIN_WITH_NGROK}/api/google-ads/upload-conversion`,
body: payload,
retries: 3,
deduplicationId: `google-ads-${payload.workspaceId}-${payload.eventId}`,
});The background API route handler POST is wrapped with withCron and parses the incoming request body against googleAdsConversionUploadSchema, immediately invoking uploadGoogleAdsConversion(payload).
export const POST = withCron(async ({ rawBody }) => {
const payload = googleAdsConversionUploadSchema.parse(JSON.parse(rawBody));
const { message, status } = await uploadGoogleAdsConversion(payload);
if (status === "failed") {
return logAndRespond(message, { status: 500, logLevel: "error" });
}
return logAndRespond(message, {
logLevel: status === "skipped" ? "warn" : "info",
});
});The core upload worker function uploadGoogleAdsConversion() executes a robust sequence of checks, token acquisition, and retry-backed API transmissions.
The worker queries Prisma for the InstalledIntegration matching GOOGLE_ADS_INTEGRATION_ID and the workspace ID, parses its settings, and resolves the conversion mapping based on eventType (lead or sale) and eventName. It acquires an OAuth access token, sets up the GoogleAdsApi instance with credentials and optional loginCustomerId, and loops up to 3 retry attempts with exponential backoff (1000 * Math.pow(2, attempt)).
Errors encountered during queueing or upload execution are captured by Axiom loggers with structured correlation metadata. If queueing fails, queueGoogleAdsConversionUpload() logs the error under google-ads.queue_conversion_failed, flushes the logger, and rethrows the error. If upload processing encounters an unhandled exception after exhausting all retry attempts, uploadGoogleAdsConversion() records an error log under google-ads.upload_conversion_failed and flushes the logger.
Sources: apps/web/lib/integrations/google-ads/upload-conversion.ts:83-97, apps/web/lib/integrations/google-ads/upload-conversion.ts:229-244