---
title: "A/B Testing and Targeting"
description: "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 ..."
last_updated: "2026-10-05T05:07:35.15703+00:00"
canonical_url: "https://www.doc0.dev/docs/934e554a-e6a1-476f-bb2f-23e62d86c3fd/technical/link-management/a-b-testing-and-targeting"
---

<details>
<summary>Relevant source files</summary>

The following files were used as context for generating this wiki page:

- [apps/web/lib/middleware/link.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/middleware/link.ts)
- [apps/web/lib/middleware/utils/resolve-ab-test-url.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/middleware/utils/resolve-ab-test-url.ts)
- [apps/web/ui/modals/link-builder/targeting-modal.tsx](https://github.com/blade47/dub/blob/HEAD/apps/web/ui/modals/link-builder/targeting-modal.tsx)
- [apps/web/app/ee/api/track/application/route.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/track/application/route.ts)
- [apps/web/middleware.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/middleware.ts)
- [apps/web/app/ee/api/track/click/route.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/track/click/route.ts)
- [apps/web/app/api/domains/route.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/app/api/domains/route.ts)
- [apps/web/app/ee/api/track/visit/route.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/track/visit/route.ts)
- [apps/web/app/api/links/random/route.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/app/api/links/random/route.ts)
- [apps/web/ui/modals/link-builder/ab-testing/ab-testing-modal.tsx](https://github.com/blade47/dub/blob/HEAD/apps/web/ui/modals/link-builder/ab-testing/ab-testing-modal.tsx)
- [apps/web/ui/modals/link-builder/ab-testing-modal.tsx](https://github.com/blade47/dub/blob/HEAD/apps/web/ui/modals/link-builder/ab-testing-modal.tsx)
- [apps/web/app/ee/api/track/open/route.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/track/open/route.ts)
- [apps/web/app/ee/api/cron/domains/update/route.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/cron/domains/update/route.ts)
- [apps/web/ui/links/link-tests.tsx](https://github.com/blade47/dub/blob/HEAD/apps/web/ui/links/link-tests.tsx)
- [apps/web/app/api/providers/route.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/app/api/providers/route.ts)
- [apps/web/app/api/links/iframeable/route.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/app/api/links/iframeable/route.ts)
- [apps/web/app/api/links/metatags/route.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/app/api/links/metatags/route.ts)
- [apps/web/lib/api/links/complete-ab-tests.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/api/links/complete-ab-tests.ts)
- [packages/utils/src/constants/dub-domains.ts](https://github.com/blade47/dub/blob/HEAD/packages/utils/src/constants/dub-domains.ts)
- [apps/web/app/app.dub.co/deeplink/deeplink/domain/...key/page.tsx](https://github.com/blade47/dub/blob/HEAD/apps/web/app/app.dub.co/(deeplink)/deeplink/%5Bdomain%5D/%5B%5B...key%5D%5D/page.tsx)
- [apps/web/lib/tinybird/record-click.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/tinybird/record-click.ts)
- [apps/web/lib/webhook/sample-events/link-clicked.json](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/webhook/sample-events/link-clicked.json)
- [apps/web/lib/middleware/utils/crawl-bitly.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/middleware/utils/crawl-bitly.ts)
- [apps/web/ui/partners/program-link-configuration.tsx](https://github.com/blade47/dub/blob/HEAD/apps/web/ui/partners/program-link-configuration.tsx)
- [apps/web/app/app.dub.co/onboarding/onboarding/steps/domain/default-domain-selector.tsx](https://github.com/blade47/dub/blob/HEAD/apps/web/app/app.dub.co/(onboarding)/onboarding/(steps)/domain/default-domain-selector.tsx)
- [apps/web/ui/placeholders/feature-graphics/domains.tsx](https://github.com/blade47/dub/blob/HEAD/apps/web/ui/placeholders/feature-graphics/domains.tsx)
- [apps/web/ui/links/tests-badge.tsx](https://github.com/blade47/dub/blob/HEAD/apps/web/ui/links/tests-badge.tsx)
- [apps/web/lib/middleware/create-link.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/middleware/create-link.ts)
- [apps/web/app/app.dub.co/onboarding/onboarding/steps/products/product-selector.tsx](https://github.com/blade47/dub/blob/HEAD/apps/web/app/app.dub.co/(onboarding)/onboarding/(steps)/products/product-selector.tsx)
- [apps/web/app/domain/key/stats/page.tsx](https://github.com/blade47/dub/blob/HEAD/apps/web/app/%5Bdomain%5D/%5Bkey%5D/stats/page.tsx)
</details>

## Overview

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](https://github.com/blade47/dub/blob/HEAD/apps/web/ui/modals/link-builder/ab-testing-modal.tsx#L244-L246), [apps/web/lib/middleware/link.ts:150-174](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/middleware/link.ts#L150-L174)

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](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/middleware/utils/resolve-ab-test-url.ts#L8-L36), [apps/web/lib/api/links/complete-ab-tests.ts:12-32](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/api/links/complete-ab-tests.ts#L12-L32), [apps/web/ui/links/link-tests.tsx:11-121](https://github.com/blade47/dub/blob/HEAD/apps/web/ui/links/link-tests.tsx#L11-L121)

## Edge Link Routing and Evaluation

### Overview

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](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/middleware/link.ts#L43-L44), [apps/web/middleware.ts:34-35](https://github.com/blade47/dub/blob/HEAD/apps/web/middleware.ts#L34-L35)

### Execution Call-Chain

The evaluation flow follows a deterministic sequence of helper functions and cache checks before resolving the final destination URL:

1. `parse(req)` extracts domain, fullKey, and search parameters from the request. Sources: [apps/web/lib/middleware/link.ts:44](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/middleware/link.ts#L44)
2. `punyEncode(originalKey)` and domain case-sensitivity checks normalize the link key. Sources: [apps/web/lib/middleware/link.ts:52-56](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/middleware/link.ts#L52-L56)
3. `linkCache.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-104](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/middleware/link.ts#L89-L104)
4. Extracted link properties (`testVariants`, `testCompletedAt`) are passed directly into `resolveABTestURL()`. Sources: [apps/web/lib/middleware/link.ts:150-171](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/middleware/link.ts#L150-L171)
5. The resolved `testUrl` overrides the default `cachedLink.url` when active test variants are present. Sources: [apps/web/lib/middleware/link.ts:173](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/middleware/link.ts#L173)

Sources: [apps/web/lib/middleware/link.ts:44-173](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/middleware/link.ts#L44-L173)

> [!NOTE]
> 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](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/middleware/link.ts#L93-L98), [apps/web/lib/middleware/link.ts:193](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/middleware/link.ts#L193)

### Middleware Routing Constants

The edge middleware matches requests against specific hostnames and path prefixes before invoking link resolution logic.

| Hostname or Prefix | Handler Function / Action | Target / Purpose | Sources |
| :--- | :--- | :--- | :--- |
| `isAppHostname(domain)` | `AppMiddleware(req)` | Handles requests for `app.dub.co` | [apps/web/middleware.ts:42-44](https://github.com/blade47/dub/blob/HEAD/apps/web/middleware.ts#L42-L44) |
| `API_HOSTNAMES.has(domain)` | `ApiMiddleware(req)` | Handles public API requests | [apps/web/middleware.ts:47-49](https://github.com/blade47/dub/blob/HEAD/apps/web/middleware.ts#L47-L49) |
| `path.startsWith("/stats/")` | `NextResponse.rewrite(...)` | Rewrites public stats page requests | [apps/web/middleware.ts:52-59](https://github.com/blade47/dub/blob/HEAD/apps/web/middleware.ts#L52-L59) |
| `path.startsWith("/.well-known/")` | `NextResponse.rewrite(...)` | Serves verified well-known configuration files | [apps/web/middleware.ts:61-69](https://github.com/blade47/dub/blob/HEAD/apps/web/middleware.ts#L61-L69) |
| `ADMIN_HOSTNAMES.has(domain)` | `AdminMiddleware(req)` | Handles administrative dashboard routes | [apps/web/middleware.ts:76-78](https://github.com/blade47/dub/blob/HEAD/apps/web/middleware.ts#L76-L78) |
| `PARTNERS_HOSTNAMES.has(domain)` | `PartnersMiddleware(req)` | Handles partner program portal requests | [apps/web/middleware.ts:80-82](https://github.com/blade47/dub/blob/HEAD/apps/web/middleware.ts#L80-L82) |
| `isValidUrl(fullKey)` | `CreateLinkMiddleware(req)` | Handles direct short link creation shortcuts | [apps/web/middleware.ts:84-86](https://github.com/blade47/dub/blob/HEAD/apps/web/middleware.ts#L84-L86) |

Sources: [apps/web/middleware.ts:41-86](https://github.com/blade47/dub/blob/HEAD/apps/web/middleware.ts#L41-L86)

## A/B Test Variant Resolution

### Overview

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](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/middleware/utils/resolve-ab-test-url.ts#L6-L14)

### Execution Call-Chain

The resolution process evaluates preconditions, checks client cookies, computes cumulative distribution weights, and selects a destination URL through a structured sequence of steps:

1. Initial validation checks that `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-22](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/middleware/utils/resolve-ab-test-url.ts#L16-L22)
2. Array boundary validation confirms that `testVariants.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-27](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/middleware/utils/resolve-ab-test-url.ts#L24-L27)
3. Cookie retrieval checks `cookieStore.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-36](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/middleware/utils/resolve-ab-test-url.ts#L29-L36)
4. Cumulative weights generation iterates through `testVariants` 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-44](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/middleware/utils/resolve-ab-test-url.ts#L38-L44)
5. Weighted random selection generates a random number between `0` and the total cumulative weight (`weights[weights.length - 1]`) using `Math.random()`. Sources: [apps/web/lib/middleware/utils/resolve-ab-test-url.ts:46-47](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/middleware/utils/resolve-ab-test-url.ts#L46-L47)
6. Variant matching loops through `weights`, 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-58](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/middleware/utils/resolve-ab-test-url.ts#L49-L58)

Sources: [apps/web/lib/middleware/utils/resolve-ab-test-url.ts:8-64](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/middleware/utils/resolve-ab-test-url.ts#L8-L64)

> [!WARNING]
> 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](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/middleware/utils/resolve-ab-test-url.ts#L16-L22)

> [!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](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/middleware/utils/resolve-ab-test-url.ts#L29-L36)

### Resolution Constants and Schema Parameters

| Parameter / Constant | Source Definition | Purpose / Constraint | Sources |
| :--- | :--- | :--- | :--- |
| `MAX_TEST_COUNT` | `ABTestVariantsSchema` import | Enforces upper bound limit on allowable test variants per link | [apps/web/lib/middleware/utils/resolve-ab-test-url.ts:1](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/middleware/utils/resolve-ab-test-url.ts#L1), [apps/web/lib/middleware/utils/resolve-ab-test-url.ts:24-27](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/middleware/utils/resolve-ab-test-url.ts#L24-L27) |
| `dub_test_url` | `cookieStore.get("dub_test_url")` | Cookie name storing the user's sticky test destination URL | [apps/web/lib/middleware/utils/resolve-ab-test-url.ts:30](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/middleware/utils/resolve-ab-test-url.ts#L30) |
| Min test variants | `testVariants.length < 2` | Requires at least two variants to perform an A/B test split | [apps/web/lib/middleware/utils/resolve-ab-test-url.ts:24-27](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/middleware/utils/resolve-ab-test-url.ts#L24-L27) |

Sources: [apps/web/lib/middleware/utils/resolve-ab-test-url.ts:1-64](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/middleware/utils/resolve-ab-test-url.ts#L1-L64)

## Targeting Rules and UTM Inheritance

### Overview

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](https://github.com/blade47/dub/blob/HEAD/apps/web/ui/modals/link-builder/targeting-modal.tsx#L67-L83)

### UTM Parameter Propagation Call Chain

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.

1. `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-74](https://github.com/blade47/dub/blob/HEAD/apps/web/ui/modals/link-builder/targeting-modal.tsx#L68-L74)
2. `UTM_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-77](https://github.com/blade47/dub/blob/HEAD/apps/web/ui/modals/link-builder/targeting-modal.tsx#L76-L77)
3. `map(...)` 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-80](https://github.com/blade47/dub/blob/HEAD/apps/web/ui/modals/link-builder/targeting-modal.tsx#L78-L80)
4. `constructURLFromUTMParams(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](https://github.com/blade47/dub/blob/HEAD/apps/web/ui/modals/link-builder/targeting-modal.tsx#L220-L235), [apps/web/ui/modals/link-builder/targeting-modal.tsx:287-296](https://github.com/blade47/dub/blob/HEAD/apps/web/ui/modals/link-builder/targeting-modal.tsx#L287-L296), [apps/web/ui/modals/link-builder/targeting-modal.tsx:319-328](https://github.com/blade47/dub/blob/HEAD/apps/web/ui/modals/link-builder/targeting-modal.tsx#L319-L328)

Sources: [apps/web/ui/modals/link-builder/targeting-modal.tsx:67-83](https://github.com/blade47/dub/blob/HEAD/apps/web/ui/modals/link-builder/targeting-modal.tsx#L67-L83), [apps/web/ui/modals/link-builder/targeting-modal.tsx:220-328](https://github.com/blade47/dub/blob/HEAD/apps/web/ui/modals/link-builder/targeting-modal.tsx#L220-L328)

> [!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](https://github.com/blade47/dub/blob/HEAD/apps/web/ui/modals/link-builder/targeting-modal.tsx#L67-L83)

### Targeting Fields and Actions Reference

| Field / Action | Form Identifier / Register | Behavior / Constraint | Sources |
| :--- | :--- | :--- | :--- |
| Geographic Targeting | `geo` | Dynamic key-value pairs mapping location codes to destination URLs with blur inheritance and deletion handlers | [apps/web/ui/modals/link-builder/targeting-modal.tsx:204-248](https://github.com/blade47/dub/blob/HEAD/apps/web/ui/modals/link-builder/targeting-modal.tsx#L204-L248) |
| Add Location Button | `setValue("geo", ...)` | Appends an empty key-value pair (`{ "": "" }`) to `geo`; disabled when an empty key already exists | [apps/web/ui/modals/link-builder/targeting-modal.tsx:253-266](https://github.com/blade47/dub/blob/HEAD/apps/web/ui/modals/link-builder/targeting-modal.tsx#L253-L266) |
| iOS Targeting | `ios` | Device-specific redirect input registered with blur-triggered UTM parameter propagation | [apps/web/ui/modals/link-builder/targeting-modal.tsx:282-298](https://github.com/blade47/dub/blob/HEAD/apps/web/ui/modals/link-builder/targeting-modal.tsx#L282-L298) |
| Android Targeting | `android` | Device-specific redirect input registered with blur-triggered UTM parameter propagation | [apps/web/ui/modals/link-builder/targeting-modal.tsx:314-330](https://github.com/blade47/dub/blob/HEAD/apps/web/ui/modals/link-builder/targeting-modal.tsx#L314-L330) |
| Remove Targeting | `parentEnabled` check | Resets `ios`, `android`, and `geo` parent values to `null` and closes the modal | [apps/web/ui/modals/link-builder/targeting-modal.tsx:337-350](https://github.com/blade47/dub/blob/HEAD/apps/web/ui/modals/link-builder/targeting-modal.tsx#L337-L350) |

Sources: [apps/web/ui/modals/link-builder/targeting-modal.tsx:204-350](https://github.com/blade47/dub/blob/HEAD/apps/web/ui/modals/link-builder/targeting-modal.tsx#L204-L350)

## Test Variant Builder and Allocation

### Overview

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](https://github.com/blade47/dub/blob/HEAD/apps/web/ui/modals/link-builder/ab-testing/ab-testing-modal.tsx#L49-L86), [apps/web/ui/modals/link-builder/ab-testing-modal.tsx:194-200](https://github.com/blade47/dub/blob/HEAD/apps/web/ui/modals/link-builder/ab-testing-modal.tsx#L194-L200)

### Variant Allocation and Mutation Logic

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.

1. `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-186](https://github.com/blade47/dub/blob/HEAD/apps/web/ui/modals/link-builder/ab-testing/ab-testing-modal.tsx#L143-L186)
2. `removeTestUrl(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-233](https://github.com/blade47/dub/blob/HEAD/apps/web/ui/modals/link-builder/ab-testing/ab-testing-modal.tsx#L188-L233)
3. `TrafficSplitSlider`: 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-362](https://github.com/blade47/dub/blob/HEAD/apps/web/ui/modals/link-builder/ab-testing/ab-testing-modal.tsx#L353-L362)

Sources: [apps/web/ui/modals/link-builder/ab-testing/ab-testing-modal.tsx:143-233](https://github.com/blade47/dub/blob/HEAD/apps/web/ui/modals/link-builder/ab-testing/ab-testing-modal.tsx#L143-L233), [apps/web/ui/modals/link-builder/ab-testing/ab-testing-modal.tsx:353-362](https://github.com/blade47/dub/blob/HEAD/apps/web/ui/modals/link-builder/ab-testing/ab-testing-modal.tsx#L353-L362)

> [!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](https://github.com/blade47/dub/blob/HEAD/apps/web/ui/modals/link-builder/ab-testing-modal.tsx#L207-L232)

### A/B Testing Modal Configuration Reference

| Component / Function | Register / Identifier | Purpose and Behavior | Sources |
| :--- | :--- | :--- | :--- |
| `ABTestingModal` | Root container | Renders the modal dialog with class `sm:max-w-md` based on `showABTestingModal` boolean state | [apps/web/ui/modals/link-builder/ab-testing/ab-testing-modal.tsx:49-65](https://github.com/blade47/dub/blob/HEAD/apps/web/ui/modals/link-builder/ab-testing/ab-testing-modal.tsx#L49-L65) |
| `ABTestingModalInner` | Form context watch | Inspects `testVariants` and `testCompletedAt` to render either completion status or editing view | [apps/web/ui/modals/link-builder/ab-testing/ab-testing-modal.tsx:67-86](https://github.com/blade47/dub/blob/HEAD/apps/web/ui/modals/link-builder/ab-testing/ab-testing-modal.tsx#L67-L86) |
| Testing URL Input | `testVariants.{index}.url` | URL input validated via `isValidUrl` and checked against `MAX_TEST_COUNT` constraints | [apps/web/ui/modals/link-builder/ab-testing-modal.tsx:287-306](https://github.com/blade47/dub/blob/HEAD/apps/web/ui/modals/link-builder/ab-testing-modal.tsx#L287-L306) |
| Traffic Splitter | `testVariants.{index}.percentage` | Visual slider component adjusting relative traffic allocation per destination URL | [apps/web/ui/modals/link-builder/ab-testing-modal.tsx:353-362](https://github.com/blade47/dub/blob/HEAD/apps/web/ui/modals/link-builder/ab-testing-modal.tsx#L353-L362) |

Sources: [apps/web/ui/modals/link-builder/ab-testing/ab-testing-modal.tsx:49-86](https://github.com/blade47/dub/blob/HEAD/apps/web/ui/modals/link-builder/ab-testing/ab-testing-modal.tsx#L49-L86), [apps/web/ui/modals/link-builder/ab-testing-modal.tsx:287-362](https://github.com/blade47/dub/blob/HEAD/apps/web/ui/modals/link-builder/ab-testing-modal.tsx#L287-L362)

## Experiment Lifecycle and Completion

### Overview

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](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/api/links/complete-ab-tests.ts#L12-L90)

### Call-Chain Execution Walkthrough

When an A/B test concludes, `completeABTests(link)` executes a sequential pipeline to evaluate analytics, resolve a winner, and persist state changes:

1. Guard validation checks that `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-15](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/api/links/complete-ab-tests.ts#L12-L15)
2. `ABTestVariantsSchema.parse(link.testVariants)` validates and types the variant configuration array. Sources: [apps/web/lib/api/links/complete-ab-tests.ts:17](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/api/links/complete-ab-tests.ts#L17)
3. `getAnalytics()` 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-26](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/api/links/complete-ab-tests.ts#L19-L26)
4. `Math.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-40](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/api/links/complete-ab-tests.ts#L28-L40)
5. `testVariants.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-55](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/api/links/complete-ab-tests.ts#L42-L55)
6. If `winner.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-73](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/api/links/complete-ab-tests.ts#L57-L73)
7. `waitUntil()` 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-89](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/api/links/complete-ab-tests.ts#L75-L89)

Sources: [apps/web/lib/api/links/complete-ab-tests.ts:12-89](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/api/links/complete-ab-tests.ts#L12-L89)

> [!CAUTION]
> 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](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/api/links/complete-ab-tests.ts#L28-L56)

### Lifecycle Completion API Reference

| Function / Utility | Parameter / Input | Operation and Output | Sources |
| :--- | :--- | :--- | :--- |
| `completeABTests` | `link: Link` | Validates completion flags, fetches analytics, computes winner, updates database, and triggers background webhooks | [apps/web/lib/api/links/complete-ab-tests.ts:12-90](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/api/links/complete-ab-tests.ts#L12-L90) |
| `ABTestVariantsSchema` | `link.testVariants` | Zod schema parsing variant configuration payloads | [apps/web/lib/api/links/complete-ab-tests.ts:5](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/api/links/complete-ab-tests.ts#L5), [apps/web/lib/api/links/complete-ab-tests.ts:17](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/api/links/complete-ab-tests.ts#L17) |
| `getAnalytics` | Query configuration object | Fetches lead counts grouped by `top_base_urls` for the experiment window | [apps/web/lib/api/links/complete-ab-tests.ts:19-26](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/api/links/complete-ab-tests.ts#L19-L26) |
| `prisma.link.update` | `{ where: { id }, data: { url }, include }` | Persists the winning variant URL and fetches associated tags, program enrollments, and project relations | [apps/web/lib/api/links/complete-ab-tests.ts:61-73](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/api/links/complete-ab-tests.ts#L61-L73) |
| `waitUntil` | `Promise.allSettled([...])` | Vercel functions helper ensuring asynchronous cache updates, Tinybird recording, and webhook dispatch complete safely | [apps/web/lib/api/links/complete-ab-tests.ts:7](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/api/links/complete-ab-tests.ts#L7), [apps/web/lib/api/links/complete-ab-tests.ts:75-89](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/api/links/complete-ab-tests.ts#L75-L89) |

Sources: [apps/web/lib/api/links/complete-ab-tests.ts:1-90](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/api/links/complete-ab-tests.ts#L1-L90)

## Link Dashboard Analytics and Badging

### Overview

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`.

Sources: [apps/web/ui/links/link-tests.tsx:1-29](https://github.com/blade47/dub/blob/HEAD/apps/web/ui/links/link-tests.tsx#L1-L29), [apps/web/ui/links/tests-badge.tsx:1-44](https://github.com/blade47/dub/blob/HEAD/apps/web/ui/links/tests-badge.tsx#L1-L44)

### Analytics Retrieval and Metric Rendering

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`.

```typescript
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,
  },
);
```

Sources: [apps/web/ui/links/link-tests.tsx:31-55](https://github.com/blade47/dub/blob/HEAD/apps/web/ui/links/link-tests.tsx#L31-L55)

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.

Sources: [apps/web/ui/links/link-tests.tsx:57-117](https://github.com/blade47/dub/blob/HEAD/apps/web/ui/links/link-tests.tsx#L57-L117)

> [!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](https://github.com/blade47/dub/blob/HEAD/apps/web/ui/links/link-tests.tsx#L31-L55)

### Component and UI Contract Reference

| Component / Utility | File Path | Primary Function and Behavior | Sources |
| :--- | :--- | :--- | :--- |
| `TestsBadge` | `apps/web/ui/links/tests-badge.tsx` | Renders a Radix hover card and interactive button with a `Flask` icon to toggle test visibility state (`showTests`). | [apps/web/ui/links/tests-badge.tsx:9-44](https://github.com/blade47/dub/blob/HEAD/apps/web/ui/links/tests-badge.tsx#L9-L44) |
| `LinkTests` | `apps/web/ui/links/link-tests.tsx` | Validates completion dates, parses variant schemas, and animates height expansion (`motion.div`). | [apps/web/ui/links/link-tests.tsx:11-121](https://github.com/blade47/dub/blob/HEAD/apps/web/ui/links/link-tests.tsx#L11-L121) |
| `ABTestVariantsSchema` | `apps/web/zod/schemas/links.ts` | Zod schema used to parse and validate link `testVariants` payloads. | [apps/web/ui/links/link-tests.tsx:2](https://github.com/blade47/dub/blob/HEAD/apps/web/ui/links/link-tests.tsx#L2) |
| `LinkAnalyticsBadge` | `apps/web/ui/links/link-analytics-badge.tsx` | Renders individual variant performance metrics including clicks, leads, sales, and revenue amounts. | [apps/web/ui/links/link-tests.tsx:102-109](https://github.com/blade47/dub/blob/HEAD/apps/web/ui/links/link-tests.tsx#L102-L109) |

Sources: [apps/web/ui/links/link-tests.tsx:1-121](https://github.com/blade47/dub/blob/HEAD/apps/web/ui/links/link-tests.tsx#L1-L121), [apps/web/ui/links/tests-badge.tsx:1-44](https://github.com/blade47/dub/blob/HEAD/apps/web/ui/links/tests-badge.tsx#L1-L44)

## Related

- [Link Creation and Builder UI](https://www.doc0.dev/docs/934e554a-e6a1-476f-bb2f-23e62d86c3fd/technical/link-management/link-creation-and-builder-ui)
- [Link Resolution and Redirection](https://www.doc0.dev/docs/934e554a-e6a1-476f-bb2f-23e62d86c3fd/technical/link-management/link-resolution-and-redirection)


## Sitemap

See the full [sitemap](https://www.doc0.dev/docs/934e554a-e6a1-476f-bb2f-23e62d86c3fd/llms.txt) for all pages in this wiki.
