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:
A/B testing and targeting empower you to optimize short link destinations by routing incoming traffic across multiple weighted destination URLs or segmenting users based on geographic location and device type. This system enables data-driven link optimization, automated experiment lifecycles, and granular conversion tracking directly within your link management workflow. Sources: apps/web/ui/modals/link-builder/ab-testing-modal.tsx:244-246, apps/web/lib/middleware/link.ts:150-174
By combining edge-level request evaluation with flexible modal controls and real-time analytics badging, the platform seamlessly handles traffic splitting, parameter inheritance, and automatic winner selection once an experiment concludes. Sources: apps/web/lib/middleware/utils/resolve-ab-test-url.ts:8-36, apps/web/lib/api/links/complete-ab-tests.ts:12-32, apps/web/ui/links/link-tests.tsx:11-121
Incoming short link requests are intercepted by Next.js middleware at the edge and routed through LinkMiddleware, where cached link metadata, targeting rules, and A/B test variants are evaluated. The execution flow begins when the request enters middleware() in apps/web/middleware.ts, which parses the incoming request to extract the domain, path, and key. Sources: apps/web/lib/middleware/link.ts:43-44, apps/web/middleware.ts:34-35
The evaluation flow follows a deterministic sequence of helper functions and cache checks before resolving the final destination URL:
parse(req) extracts domain, fullKey, and search parameters from the request. Sources: apps/web/lib/middleware/link.ts:44punyEncode(originalKey) and domain case-sensitivity checks normalize the link key. Sources: apps/web/lib/middleware/link.ts:52-56linkCache.get({ domain, key }) retrieves cached link properties or triggers a fallback via getLinkViaEdge({ domain, key }) if the cache misses. Sources: apps/web/lib/middleware/link.ts:89-104testVariants, testCompletedAt) are passed directly into resolveABTestURL(). Sources: apps/web/lib/middleware/link.ts:150-171testUrl overrides the default cachedLink.url when active test variants are present. Sources: apps/web/lib/middleware/link.ts:173Note
During Redis failover events (redisFailOver === true), click tracking jobs are bypassed to prevent request timeouts, and cookie minting for conversion tracking is suspended. Sources: apps/web/lib/middleware/link.ts:93-98, apps/web/lib/middleware/link.ts:193
The edge middleware matches requests against specific hostnames and path prefixes before invoking link resolution logic.
Sources: apps/web/middleware.ts:41-86
The A/B testing destination selection is handled by resolveABTestURL, an asynchronous utility function that performs either cookie-driven sticky routing or weighted random percentage traffic splitting across configured test variant URLs. Sources: apps/web/lib/middleware/utils/resolve-ab-test-url.ts:6-14
The resolution process evaluates preconditions, checks client cookies, computes cumulative distribution weights, and selects a destination URL through a structured sequence of steps:
testVariants and testCompletedAt are supplied, and that testCompletedAt is set to a future date (new Date(testCompletedAt) > new Date()). If any check fails, the function returns null. Sources: apps/web/lib/middleware/utils/resolve-ab-test-url.ts:16-22testVariants.length is between 2 and MAX_TEST_COUNT. If out of bounds, an error is logged via console.error and null is returned. Sources: apps/web/lib/middleware/utils/resolve-ab-test-url.ts:24-27cookieStore.get("dub_test_url")?.value. If a cookie exists and its value matches one of the valid variant URLs in testVariants, sticky routing returns urlFromCookie immediately, bypassing random assignment. Sources: apps/web/lib/middleware/utils/resolve-ab-test-url.ts:29-36testVariants starting at index 1, adding each variant's percentage to the preceding cumulative sum in weights. Sources: apps/web/lib/middleware/utils/resolve-ab-test-url.ts:38-440 and the total cumulative weight (weights[weights.length - 1]) using Math.random(). Sources: apps/web/lib/middleware/utils/resolve-ab-test-url.ts:46-47weights, finding the first index where weights[i] > random, logs the selected variant via console.log, and returns testVariants[i].url. Sources: apps/web/lib/middleware/utils/resolve-ab-test-url.ts:49-58Warning
If testCompletedAt is in the past or omitted, resolveABTestURL immediately returns null, short-circuiting active test variant evaluation even if variants are populated. Sources: apps/web/lib/middleware/utils/resolve-ab-test-url.ts:16-22
Note
Sticky session persistence relies entirely on the dub_test_url cookie value. If a user clears cookies or visits from a new browser session, they will be re-allocated randomly according to the percentage traffic weights. Sources: apps/web/lib/middleware/utils/resolve-ab-test-url.ts:29-36
The targeting modal UI configures geographic (geo) and device-specific (ios, android) redirection rules for shortened links. It includes parameter propagation logic that inspects the parent URL's UTM parameters on blur events and automatically appends missing parameters to the target URL. Sources: apps/web/ui/modals/link-builder/targeting-modal.tsx:67-83
When a user finishes editing a targeting URL input and triggers a blur event, the application executes a specific sequence of utility functions to propagate missing marketing parameters.
getNewParams(targetURL): Validates that targetURL is non-empty and well-formed via isValidUrl(targetURL), retrieves parent URL parameters using getParamsFromURL(parentUrl), and inspects target parameters via getParamsFromURL(targetURL). Sources: apps/web/ui/modals/link-builder/targeting-modal.tsx:68-74UTM_PARAMETERS.filter(...): Iterates over defined UTM keys, checking whether parentParams?.[key] exists while !targetParams?.[key] is true. Sources: apps/web/ui/modals/link-builder/targeting-modal.tsx:76-77map(...) and Object.fromEntries(...): Transforms filtered key-value pairs into an object dictionary of missing parameters, returning null if no parameters require propagation. Sources: apps/web/ui/modals/link-builder/targeting-modal.tsx:78-80constructURLFromUTMParams(value, newParams): Appends the resolved newParams dictionary to the target URL if newParams evaluates to a non-null object during input blur handling for iOS, Android, or geographic targets. Sources: apps/web/ui/modals/link-builder/targeting-modal.tsx:220-235, apps/web/ui/modals/link-builder/targeting-modal.tsx:287-296, apps/web/ui/modals/link-builder/targeting-modal.tsx:319-328Sources: apps/web/ui/modals/link-builder/targeting-modal.tsx:67-83, apps/web/ui/modals/link-builder/targeting-modal.tsx:220-328
Tip
Parameter inheritance is deferred until the input loses focus (onBlur). This allows users to freely type or paste target URLs without interference from automatic query parameter injection. Sources: apps/web/ui/modals/link-builder/targeting-modal.tsx:67-83
The A/B testing link builder modal provides UI controls for configuring test destination variants, adjusting percentage traffic allocations, and specifying test completion timelines. The modal is encapsulated within ABTestingModal, rendering ABTestingModalInner which conditionally switches between ABTestingComplete and ABTestingEdit depending on whether an existing test's completion date (testCompletedAt) has passed. Sources: apps/web/ui/modals/link-builder/ab-testing/ab-testing-modal.tsx:49-86, apps/web/ui/modals/link-builder/ab-testing-modal.tsx:194-200
When editing test variants inside ABTestingEdit, users can add or remove variant URLs up to configured limits. The allocation workflow handles uniform distribution and unequal splitting using specific state update routines.
addTestUrl(): Validates that testVariants.length is less than MAX_TEST_COUNT. If all existing variants have equal percentage shares (allEqual), it recalculates new equal shares using Math.floor(100 / (testVariants.length + 1)) and assigns the remainder to the new entry. If percentages are unequal, it locates the last variant with a percentage greater than or equal to MIN_TEST_PERCENTAGE * 2 (toSplitIndex), halves its percentage, and assigns the split remainder to the new variant URL. Sources: apps/web/ui/modals/link-builder/ab-testing/ab-testing-modal.tsx:143-186removeTestUrl(index): Requires at least 2 variants (testVariants.length < 2 aborts). If percentages are equal, it reallocates equal shares across the remaining count. If unequal, it filters out the target index and adds the removed variant's percentage (remainder) onto the final item in the array. Sources: apps/web/ui/modals/link-builder/ab-testing/ab-testing-modal.tsx:188-233TrafficSplitSlider: Passes the active testVariants array and an onChange callback that iterates over updated percentage values, updating each variant via setValue(..., { shouldDirty: true }). Sources: apps/web/ui/modals/link-builder/ab-testing/ab-testing-modal.tsx:353-362Sources: apps/web/ui/modals/link-builder/ab-testing/ab-testing-modal.tsx:143-233, apps/web/ui/modals/link-builder/ab-testing/ab-testing-modal.tsx:353-362
Warning
Submitting the form triggers strict validation rules: if the variant count drops to one or zero, testVariants and testCompletedAt are reset to null. Otherwise, the form validates that totalPercentage strictly equals 100 and that every variant contains a non-empty url string before saving changes to parent form state. Sources: apps/web/ui/modals/link-builder/ab-testing-modal.tsx:207-232
Sources: apps/web/ui/modals/link-builder/ab-testing/ab-testing-modal.tsx:49-86, apps/web/ui/modals/link-builder/ab-testing-modal.tsx:287-362
The experiment completion subsystem evaluates performance metrics for concluded A/B tests, determines winning destination URLs based on lead conversion counts, updates persistent database records, and dispatches asynchronous lifecycle events. This flow is orchestrated by the completeABTests utility function. Sources: apps/web/lib/api/links/complete-ab-tests.ts:12-90
When an A/B test concludes, completeABTests(link) executes a sequential pipeline to evaluate analytics, resolve a winner, and persist state changes:
link.testVariants, link.testCompletedAt, and link.projectId are all present; otherwise, execution returns early. Sources: apps/web/lib/api/links/complete-ab-tests.ts:12-15ABTestVariantsSchema.parse(link.testVariants) validates and types the variant configuration array. Sources: apps/web/lib/api/links/complete-ab-tests.ts:17getAnalytics() queries lead counts grouped by top_base_urls for the specific linkId and workspace (link.projectId), bounded between link.testStartedAt and link.testCompletedAt. Sources: apps/web/lib/api/links/complete-ab-tests.ts:19-26Math.max() computes the peak lead count across all test variants. If max === 0, execution halts and logs that all results are zero. Sources: apps/web/lib/api/links/complete-ab-tests.ts:28-40testVariants.filter() selects all variants matching the maximum lead count. If winners.length === 0, execution aborts. If multiple variants share the maximum lead count, Math.floor(Math.random() * winners.length) breaks ties uniformly at random. Sources: apps/web/lib/api/links/complete-ab-tests.ts:42-55winner.url === link.url, the process terminates. Otherwise, prisma.link.update() updates the destination url to the winner's URL while including tags, program enrollments, and project relations. Sources: apps/web/lib/api/links/complete-ab-tests.ts:57-73waitUntil() schedules background settlement via Promise.allSettled, which executes linkCache.set(response), recordLink(response), and sendWorkspaceWebhook() with trigger link.updated. Sources: apps/web/lib/api/links/complete-ab-tests.ts:75-89Caution
If multiple variants tie for the highest lead count, completeABTests uses Math.floor(Math.random() * winners.length) to select a winner uniformly at random among the top performers. If all variants record zero leads (max === 0), the test completes without mutating the link URL or changing its state. Sources: apps/web/lib/api/links/complete-ab-tests.ts:28-56
Link management interfaces expose active split test states and conversion analytics directly via companion badging components and expandable UI rows. The TestsBadge component renders a hover card and a toggle button adorned with a Flask icon, allowing operators to reveal or hide active split testing configurations from the link card context. When expanded, LinkTests inspects the link's testVariants and verifies that testCompletedAt exists and lies in the future (new Date() < new Date(link.testCompletedAt)). Valid variant structures are parsed via ABTestVariantsSchema.
Once active variants are confirmed and test visibility is enabled (showTests is true), LinkTests queries the /api/analytics endpoint using SWR with revalidateOnFocus: false. The request constructs composite parameters grouped by top_base_urls for the specific linkId and workspaceId, optionally bounding the start time to link.testStartedAt.
const { data, isLoading, error } = useSWR<
{
url: string;
clicks: number;
leads: number;
saleAmount: number;
sales: number;
}[]
>(
Boolean(testVariants && testVariants.length) &&
showTests &&
`/api/analytics?${new URLSearchParams({
event: "composite",
groupBy: "top_base_urls",
linkId: link.id,
workspaceId: workspaceId!,
...(link.testStartedAt && {
start: new Date(link.testStartedAt).toISOString(),
}),
}).toString()}`,
fetcher,
{
revalidateOnFocus: false,
},
);For each test variant iteration, the UI matches analytics data against the variant's destination URL and renders a numbered indicator, a pretty-printed URL via getPrettyUrl(), a rounded percentage badge representing the traffic allocation (Math.round(test.percentage)%), and a LinkAnalyticsBadge component populated with aggregated clicks, leads, sales, and sale amounts.
Note
The analytics fetch within LinkTests is guarded by both Boolean(testVariants && testVariants.length) and showTests. If testing visibility is toggled off or variants are absent, SWR request dispatching is skipped entirely to conserve analytics query quota. Sources: apps/web/ui/links/link-tests.tsx:31-55