Cross-project metrics

A metric normally lives in one project. An org-wide metric goes further: it can be used by experiments in any project in your organization. A cross-project metric adds one more idea on top: it reads its measured data from a single project you choose, called the source project, no matter which project the experiment runs in.

This page explains that model in plain terms, walks through the flagship use (measuring the same goal across a web experiment and a feature-flag test) and then covers the one rule you have to get right for the numbers to add up: visitor identity.

The linked-source model

Think of a shared metric as having one home for its data. That home is the source project.

  • The metric can be attached to experiments in many projects.
  • But when A vs B calculates results, it always reads that metric's events from the source project only.

One project supplies the data. Every experiment that uses the metric looks at that same pool of events. Nothing is added up twice.

What “no double counting” means

Double counting is when the same real conversion is counted more than once: for example, if a purchase were read from two projects at the same time and totalled together. The linked-source model avoids this by design: there is always exactly one source project per metric, so each conversion is counted once.

Picking a source project is opt-in

The Data source project field defaults to "Same as experiment." Until you change it, a metric's results come from whichever project the attached experiment lives in, not one fixed project. That's fine for a metric that genuinely happens separately in every project. Pin a source project only for the case this page covers: one real-world action, measured once, judged from more than one project.

Why one source project, not both

It is natural to ask why A vs B doesn't just merge the events from every project and use them all. The reason is safety.

Each recorded event carries its own random one-time id. Because those ids are unique per event, there is no reliable way to tell whether an event in project A and an event in project B are the same action or two different actions. Merging the two streams would risk counting one purchase as two. So instead of guessing, A vs B asks you to pick one source project on purpose. That choice is exact and predictable.

Example: one purchase metric across your site and your app

This is the case cross-project metrics were built for. Two quick definitions first:

  • Web experiment (WE): the A vs B snippet you paste into your website's HTML. It runs in the visitor's browser and swaps content on the page.
  • Feature-flag test (FF): the A vs B SDK you add inside your application's code. It decides, in your own code, which version each user sees.

Say your marketing site is one project and your checkout app is another. You want both a homepage web experiment and an in-app feature-flag test to be judged on the same goal: completed purchases.

  1. Create a Custom metric for completed purchases and make it org-wide so both projects can use it. (Revenue is tracked on a Custom metric with a value attached, not a separate metric type. Setting a metric to org-wide takes an organization admin: see Managing Metrics.)
  2. Set its Data source project to the project that actually records purchases: your checkout app.
  3. Attach that metric to the homepage web experiment and to the in-app feature-flag test.
  1. Choose Org-wide so every project can attach this metric.
  2. Set Data source project to the one project whose events should count, instead of leaving it on "Same as experiment."

Now both surfaces are scored against the same purchase data, read from the one source project. You get a single, consistent definition of "did they buy?" everywhere.

Change the source project later, and every experiment using the metric switches over right away. There's no snippet to redeploy: it only changes which project's events the next results calculation reads. Like any other metric edit, this affects every experiment using the metric at once. Check what's attached first (see Managing Metrics).

The identity rule: why the ids must match

Here is the honest catch, and the most important thing on this page.

A vs B joins the two surfaces together by matching visitor ids exactly. A visitor's browser activity and their in-app activity only line up if the same visitor carries the same id on both surfaces.

  • On the website, the snippet gives each browser a visitor id and stores it in the _avsb_visitor cookie. You read it with avsb.getVisitorId().
  • In the SDK, the visitor id is whatever you pass in: the bucketing key on the server SDK, or the userId attribute on the browser SDK.

If those two ids don't match for the same person, the join finds nothing in common and the cross-surface results look empty: even though both surfaces are tracking fine on their own. This is a data-wiring problem, not a bug.

Zero overlap means empty results

If the browser and the SDK use different visitor ids, cross-project results can show zero joined visitors. Before you trust a cross-surface number, confirm the same id reaches both surfaces. The full wiring guide is Shared Visitor Identity.

Quick recipe

Read the snippet's visitor id in the browser, forward it to your server, and pass it as the SDK's context key so both surfaces record the same id:

TypeScript
// 1) In the browser: read the snippet's visitor id and send it to your server.avsb.ready?.(function () {  const visitorId = avsb.getVisitorId(); // the _avsb_visitor cookie UUID  if (!visitorId) return; // no id yet: consent pending, or analytics denied  fetch('/api/checkout', {    method: 'POST',    headers: { 'x-avsb-visitor': visitorId },    body: /* ... */ null,  });});
TypeScript10 lines
TypeScript
// 2) On your server: pass that same id as the SDK context key.import { AvsbServer } from '@avsbhq/node';const server = new AvsbServer({ sdkKey: process.env.AVSB_SDK_KEY ?? '' });await server.onReady();// `req` is your framework's request object.export async function handleCheckout(req: { headers: Record<string, string> }): Promise<void> {  const visitorId = req.headers['x-avsb-visitor']; // the id from the browser  const context = { kind: 'user', key: visitorId };  const showNewCheckout = server.getFlag('new_checkout', false, context);  // When the purchase completes, track against the SAME id:  server.track('purchase', { revenue: 49.99, context });}
TypeScript16 lines

Because the SDK buckets and tracks on context.key, and the browser tracks on the _avsb_visitor id, using the same value on both sides makes the results join. The full guide (including the browser SDK and the same-domain shortcut) is on the identity page below.

Was this helpful?