Developer Demo

Optimizely Data Platform (ODP)

ODP is Optimizely's customer data platform: it builds a behavioral profile per visitor and computes real-time segment membership. This app uses it for identity stitching, event collection, and server-side segment queries that drive personalized CMS content.

What ODP does in this stack#

Three things flow through ODP here. The browser sends events (pageviews, identity) via the ODP tag. ODP aggregates those into a customer profile and evaluates segment membership ("high-value-customers", "retail-consumer"). The server then queries that membership by visitor ID and maps qualifying segments to CMS content variations - the last mile of the personalization flow.

The ODP tag (inlined in the root layout head)
// src/app/layout.tsx - the ODP tag is inlined in <head> so the zaius
// command queue exists synchronously during HTML parsing. Events fired
// before the async script loads are queued and replayed.

var zaius = window['zaius'] || (window['zaius'] = []);
zaius.methods = ['initialize','onload','customer','entity','event', /* ... */];
// ...queue shim...
e.src = 'https://d1igp3oop3iho5.cloudfront.net/v2/' +
        NEXT_PUBLIC_OPTIMIZELY_ODP_TRACKER_ID + '/zaius-min.js';

Identity stitching#

ODP tracks browsers by its own vuid cookie, but everything else in this app keys off optimizelyEndUserId (set by middleware, used by Feature Experimentation). Linking the two once via the fs_user_id identifier is what lets the server ask ODP about the same visitor the FX SDK is bucketing.

src/components/OdpSetup.tsx
// src/components/OdpSetup.tsx ("use client", rendered in the root layout)
//
// ODP assigns every browser its own vuid cookie. To query segments
// server-side using the FX visitor ID, the two identities must be linked:
// send optimizelyEndUserId to ODP as the fs_user_id identifier once.

useEffect(() => {
  const fsUserId = getCookie("optimizelyEndUserId");
  if (fsUserId) window.zaius?.entity("customer", { fs_user_id: fsUserId });
}, []);

// SPA route changes don't reload the page, so fire a pageview per navigation:
useEffect(() => {
  window.zaius?.event("pageview");
}, [pathname]);

Server-side segment queries#

ODP exposes a GraphQL API for profile data. The app asks one narrow question per request: of the segments this app cares about, which does the visitor qualify for? The subset filter keeps the query cheap, the 300s cache keeps it off the hot path, and any failure returns an empty array - personalization degrades to the default content, never to an error page.

src/lib/optimizely/odp.ts
// src/lib/optimizely/odp.ts - server-side segment membership query.
// Auth is the ODP API key in an x-api-key header (server-only env var).

const SEGMENT_QUERY = `
  query GetSegments($userId: String!, $segmentFilter: [String!]!) {
    customer(vuid: $userId) {
      audiences(subset: $segmentFilter) {
        edges { node { name state } }
      }
    }
  }
`;

export async function queryOdpSegments(userId: string): Promise<string[]> {
  const res = await fetch(`${ODP_API_HOST}/v3/graphql`, {
    method: "POST",
    headers: { "Content-Type": "application/json", "x-api-key": ODP_API_KEY },
    body: JSON.stringify({ query: SEGMENT_QUERY, variables: { userId, segmentFilter } }),
    next: { revalidate: 300 },   // segment membership changes slowly - cache it
  });
  // ...filter edges to state === "qualified", return segment names
}

Segment-to-variation mapping#

ODP segment names and CMS variation names are owned by different teams and different systems, so the contract between them is a single explicit map. First qualifying segment wins. The resolved variation key feeds Graph's variation filter exactly like an FX bucketing decision does (see variation resolution).

The one place both sides meet
// src/lib/optimizely/odp.ts
//
// The explicit contract between ODP segment names and CMS variation names.
// This is the only place to update when either side renames something.

export const ODP_SEGMENT_TO_VARIATION: Record<string, string> = {
  // "high-value-customers": "business",
  // "retail-consumer":      "personal",
};

export function resolveVariationKey(segments: string[]): string | undefined {
  for (const segment of segments) {
    const key = ODP_SEGMENT_TO_VARIATION[segment];
    if (key) return key;
  }
}

// Usage (see /demo/personalization): query the visitor's segments, resolve a
// CMS variation name, pass it to Graph's variation filter so the visitor gets
// the matching page variation - with includeOriginal: true as the fallback.

ODP events vs FX events#

This app runs two tracking pipelines that are easy to confuse. ODP events feed the behavioral profile that segments are computed from. FX events (fired by AutoTracker through the FX SDK) feed experiment metrics. They share a visitor ID but nothing else - an ODP pageview never shows up in experiment results, and an FX conversion never moves segment membership.

Two pipelines, one visitor ID
// Two event pipelines run side by side in this app - don't conflate them:
//
// 1. ODP events (behavioral profile, segments, campaigns)
//    window.zaius.event("pageview")            <- OdpSetup, per route change
//    window.zaius.entity("customer", {...})    <- identity stitching
//
// 2. FX events (experiment metrics and impressions)
//    trackEvent("mb_scroll_depth", {...})      <- AutoTracker via the FX SDK
//    user.decide("flag", [])                    <- impression on render
//
// ODP events build the profile that segments are computed from.
// FX events power experiment results. Both key off the same visitor ID
// (optimizelyEndUserId) - which is exactly why OdpSetup links the IDs.

Key Things to Know#

  • Link identities once, early. Sending fs_user_id to ODP on first load is what makes server-side segment queries by optimizelyEndUserId possible at all.
  • Query segments with a subset filter. Ask ODP only about the segments your code maps to variations - not the whole profile.
  • Cache segment membership. It changes slowly; revalidate: 300 keeps ODP off the request hot path.
  • Fail to the default content. queryOdpSegments returns [] on any error - the visitor gets the original page, not a 500.
  • Keep the segment-to-variation map in one file. ODP and the CMS name things independently; a single explicit contract is the only rename-safe design.
  • ODP events and FX events are separate pipelines. Profiles and segments come from ODP events; experiment results come from FX events.
Source files2 files
src/lib/optimizely/odp.ts
const ODP_API_HOST = process.env.OPTIMIZELY_ODP_API_HOST ?? "https://api.zaius.com";
const ODP_API_KEY  = process.env.OPTIMIZELY_ODP_API_KEY  ?? "";

const SEGMENT_QUERY = `
  query GetSegments($userId: String!, $segmentFilter: [String!]!) {
    customer(vuid: $userId) {
      audiences(subset: $segmentFilter) {
        edges { node { name state } }
      }
    }
  }
`;

// Queries ODP for the segments in ODP_SEGMENT_TO_VARIATION that the visitor qualifies for.
// The subset is derived from the mapping keys so we only ask ODP about segments we actually use.
export async function queryOdpSegments(userId: string): Promise<string[]> {
  if (!ODP_API_KEY) return [];
  const segmentFilter = Object.keys(ODP_SEGMENT_TO_VARIATION);
  if (segmentFilter.length === 0) return [];
  try {
    const res = await fetch(`${ODP_API_HOST}/v3/graphql`, {
      method: "POST",
      headers: { "Content-Type": "application/json", "x-api-key": ODP_API_KEY },
      body: JSON.stringify({ query: SEGMENT_QUERY, variables: { userId, segmentFilter } }),
      next: { revalidate: 300 },
    });
    if (!res.ok) return [];
    const data = await res.json();
    return (
      (data.data?.customer?.audiences?.edges ?? [])
        .filter((e: { node: { state: string } }) => e.node.state === "qualified")
        .map((e: { node: { name: string } }) => e.node.name)
    );
  } catch {
    return [];
  }
}

// The explicit contract between ODP segment names and CMS variation names.
// This is the only place to update when either side renames something.
export const ODP_SEGMENT_TO_VARIATION: Record<string, string> = {
  // "high-value-customers": "business",
  // "retail-consumer":      "personal",
};

export function resolveVariationKey(segments: string[]): string | undefined {
  for (const segment of segments) {
    const key = ODP_SEGMENT_TO_VARIATION[segment];
    if (key) return key;
  }
}
src/components/OdpSetup.tsx
"use client";

import { usePathname } from "next/navigation";
import { useEffect } from "react";

import "@/lib/tracking/destinations/odp";

function getCookie(name: string): string | undefined {
  return document.cookie.match(new RegExp(`(?:^|;\\s*)${name}=([^;]*)`))?.[1];
}

export default function OdpSetup() {
  const pathname = usePathname();

  // Link the FX visitor ID to ODP once on mount so server-side segment
  // queries can use optimizelyEndUserId via the fs_user_id identifier.
  useEffect(() => {
    const fsUserId = getCookie("optimizelyEndUserId");
    if (fsUserId) window.zaius?.entity("customer", { fs_user_id: fsUserId });
  }, []);

  useEffect(() => {
    window.zaius?.event("pageview");
  }, [pathname]);

  return null;
}