diff --git a/lib/addons/prebid/README.md b/lib/addons/prebid/README.md index cde1cbb..12733b3 100644 --- a/lib/addons/prebid/README.md +++ b/lib/addons/prebid/README.md @@ -4,47 +4,82 @@ This addon integrates Optable analytics with Prebid.js, allowing you to send auc ## Installation -1. **Configure Analytics** - Use the following code snippet to enable analytics and configure the integration withing your Optable SDK wrapper: - - ```js - import OptablePrebidAnalytics from "./analytics"; - - // ... - const tenant = "my_tenant"; - const analyticsSample = sessionStorage.optableEnableAnalytics || 0.1; - window.optable.runAnalytics = analyticsSample > Math.random(); - // ... - - window.optable.customAnalytics = function () { - const customAnalyticsObject = {}; - // ... - return customAnalyticsObject; - }; - - // ... - if (window.optable.runAnalytics && tenant) { - window.optable[`${tenant}_analytics`] = new window.optable.SDK({ - host: "na.edge.optable.co", - node: "analytics", - site: "analytics", - readOnly: true, - cookies: false, - }); - - window.optable.analytics = new OptablePrebidAnalytics(window.optable[`${tenant}_analytics`], { - analytics: true, - tenant, - debug: !!sessionStorage.optableDebug, - samplingRate: 0.75, - }); - window.optable.analytics.hookIntoPrebid(window.pbjs); - } - // ... - ``` - - - Replace 'my_tenant' with your Optable tenant name. - - Optionally, implement `window.optable.customAnalytics` to add custom key-value pairs to each analytics event. +### Quick start: `initPrebidAnalytics` + +`initPrebidAnalytics` bootstraps the whole integration in one call: it creates a +dedicated read-only analytics SDK instance, constructs `OptablePrebidAnalytics`, +and hooks it into the publisher's Prebid.js global. It returns the analytics +instance, or `null` when no Prebid.js global is present (in which case no SDK +instance is created). + +```ts +import OptableSDK from "@optable/web-sdk"; +import { initPrebidAnalytics } from "@optable/web-sdk/lib/dist/addons/prebid/analytics"; + +initPrebidAnalytics({ + SDK: OptableSDK, + // Config for the dedicated read-only analytics SDK instance + instance: { host: "na.edge.optable.co", node: "analytics", site: "analytics" }, + // Prebid global by name (default "pbjs"), or pass the object directly via `pbjs` + prebidGlobal: "pbjs", + // Forwarded to the OptablePrebidAnalytics constructor + analytics: { + tenant: "my_tenant", + samplingRate: 0.1, + debug: !!sessionStorage.optableDebug, + // Any OptablePrebidAnalyticsConfig option is forwarded, e.g. + // getSplitTestAssignment: () => window.optable?.selectedTest?.id, + }, +}); +``` + +The `SDK` constructor is passed in (rather than imported by this module) so that +consumers of the `OptablePrebidAnalytics` class don't bundle the whole SDK. The +`instance` config sets where analytics data is sent; `readOnly: true` and +`cookies: false` are applied by default and can be overridden through `instance`. + +### Manual setup + +For full control over the instances, wire the pieces up yourself: + +```js +import OptablePrebidAnalytics from "./analytics"; + +// ... +const tenant = "my_tenant"; +const analyticsSample = sessionStorage.optableEnableAnalytics || 0.1; +window.optable.runAnalytics = analyticsSample > Math.random(); +// ... + +window.optable.customAnalytics = function () { + const customAnalyticsObject = {}; + // ... + return customAnalyticsObject; +}; + +// ... +if (window.optable.runAnalytics && tenant) { + window.optable[`${tenant}_analytics`] = new window.optable.SDK({ + host: "na.edge.optable.co", + node: "analytics", + site: "analytics", + readOnly: true, + cookies: false, + }); + + window.optable.analytics = new OptablePrebidAnalytics(window.optable[`${tenant}_analytics`], { + analytics: true, + tenant, + debug: !!sessionStorage.optableDebug, + samplingRate: 0.75, + }); + window.optable.analytics.hookIntoPrebid(window.pbjs); +} +// ... +``` + +- Replace 'my_tenant' with your Optable tenant name. +- Optionally, implement `window.optable.customAnalytics` to add custom key-value pairs to each analytics event. ## Usage @@ -59,6 +94,16 @@ This addon integrates Optable analytics with Prebid.js, allowing you to send auc ## API +### `initPrebidAnalytics(options)` + +Bootstraps the integration and returns the `OptablePrebidAnalytics` instance, or `null` when no Prebid.js global is present. + +- `options.SDK`: The Optable SDK constructor (pass the imported `OptableSDK`). +- `options.instance`: Config for the dedicated read-only analytics SDK instance (`host`/`node`/`site`/…). `readOnly: true` and `cookies: false` default on and can be overridden here. +- `options.pbjs`: The Prebid.js global to hook into. When omitted, `window[prebidGlobal]` is used. +- `options.prebidGlobal`: Name of the prebid global on `window` (default `"pbjs"`), used when `pbjs` is not passed. +- `options.analytics`: Config forwarded to the `OptablePrebidAnalytics` constructor (`tenant`, `samplingRate`, `debug`, `getSplitTestAssignment`, …). + ### `OptablePrebidAnalytics` - **Constructor**: diff --git a/lib/addons/prebid/analytics.test.ts b/lib/addons/prebid/analytics.test.ts index c63b9a2..c3f26ed 100644 --- a/lib/addons/prebid/analytics.test.ts +++ b/lib/addons/prebid/analytics.test.ts @@ -1,4 +1,4 @@ -import OptablePrebidAnalytics from "./analytics"; +import OptablePrebidAnalytics, { initPrebidAnalytics } from "./analytics"; import type OptableSDK from "../../sdk"; // Mock the SDK_WRAPPER_VERSION global @@ -1921,3 +1921,103 @@ describe("OptablePrebidAnalytics", () => { }); }); }); + +describe("initPrebidAnalytics", () => { + // A stub SDK constructor whose instances expose witness(), enough for + // OptablePrebidAnalytics. Records the config it was constructed with. + function makeFakeSDK() { + const witness = jest.fn().mockResolvedValue(undefined); + const ctor = jest.fn().mockImplementation(() => ({ witness })); + return { ctor: ctor as unknown as new (config: any) => any, calls: ctor.mock.calls, witness }; + } + + function loadedPrebid() { + return { getEvents: jest.fn(() => []), onEvent: jest.fn() }; + } + + beforeEach(() => { + delete (window as any).pbjs; + delete (window as any).fusePbjs; + }); + + it("returns null and creates no SDK when no prebid global is present", () => { + const { ctor } = makeFakeSDK(); + const result = initPrebidAnalytics({ SDK: ctor, instance: { host: "h", site: "s" } }); + + expect(result).toBeNull(); + expect(ctor).not.toHaveBeenCalled(); + }); + + it("creates a read-only analytics SDK instance and hooks the passed prebid global", () => { + const pbjs = loadedPrebid(); + const { ctor } = makeFakeSDK(); + + const result = initPrebidAnalytics({ + SDK: ctor, + instance: { host: "h", node: "n", site: "s" }, + pbjs, + analytics: { tenant: "acme" }, + }); + + expect(result).toBeInstanceOf(OptablePrebidAnalytics); + expect(ctor).toHaveBeenCalledWith( + expect.objectContaining({ host: "h", node: "n", site: "s", readOnly: true, cookies: false }) + ); + expect(pbjs.onEvent).toHaveBeenCalledWith("auctionEnd", expect.any(Function)); + }); + + it("reads the named prebid global from window when pbjs is not passed", () => { + (window as any).fusePbjs = loadedPrebid(); + const { ctor } = makeFakeSDK(); + + initPrebidAnalytics({ + SDK: ctor, + instance: { host: "h", site: "s" }, + prebidGlobal: "fusePbjs", + analytics: { tenant: "acme" }, + }); + + expect((window as any).fusePbjs.onEvent).toHaveBeenCalledWith("auctionEnd", expect.any(Function)); + }); + + it("defaults to window.pbjs when no global is configured", () => { + (window as any).pbjs = loadedPrebid(); + const { ctor } = makeFakeSDK(); + + initPrebidAnalytics({ SDK: ctor, instance: { host: "h", site: "s" }, analytics: { tenant: "acme" } }); + + expect((window as any).pbjs.onEvent).toHaveBeenCalledWith("auctionEnd", expect.any(Function)); + }); + + it("forwards the analytics config through to the OptablePrebidAnalytics instance", () => { + const pbjs = loadedPrebid(); + const { ctor } = makeFakeSDK(); + + // Any OptablePrebidAnalyticsConfig option flows through, including + // getSplitTestAssignment once it lands in the config interface. + const result = initPrebidAnalytics({ + SDK: ctor, + instance: { host: "h", site: "s" }, + pbjs, + analytics: { tenant: "acme", samplingRate: 0.25 }, + }); + + expect(result!["config"].analytics).toBe(true); + expect(result!["config"].tenant).toBe("acme"); + expect(result!["config"].samplingRate).toBe(0.25); + }); + + it("lets the instance config override the read-only defaults", () => { + const pbjs = loadedPrebid(); + const { ctor } = makeFakeSDK(); + + initPrebidAnalytics({ + SDK: ctor, + instance: { host: "h", site: "s", readOnly: false }, + pbjs, + analytics: { tenant: "acme" }, + }); + + expect(ctor).toHaveBeenCalledWith(expect.objectContaining({ readOnly: false })); + }); +}); diff --git a/lib/addons/prebid/analytics.ts b/lib/addons/prebid/analytics.ts index f03aad3..366cbf0 100644 --- a/lib/addons/prebid/analytics.ts +++ b/lib/addons/prebid/analytics.ts @@ -2,6 +2,7 @@ /* eslint-disable no-console */ import type { WitnessProperties } from "../../edge/witness"; import type OptableSDK from "../../sdk"; +import type { InitConfig } from "../../config"; import * as Bowser from "bowser"; @@ -554,3 +555,56 @@ class OptablePrebidAnalytics { } export default OptablePrebidAnalytics; + +/** + * Options for {@link initPrebidAnalytics}. + */ +export interface InitPrebidAnalyticsOptions { + /** + * The Optable SDK constructor (pass the imported `OptableSDK`). Taken as a + * parameter so this module keeps a type-only SDK import and consumers of the + * `OptablePrebidAnalytics` class don't bundle the whole SDK. + */ + SDK: new (config: InitConfig) => OptableSDK; + /** Config for the dedicated read-only analytics SDK instance (host/node/site/…). */ + instance: InitConfig; + /** Prebid.js global to hook into. When omitted, `window[prebidGlobal]` is used. */ + pbjs?: any; + /** Name of the prebid global on `window` (default `"pbjs"`), used when `pbjs` is not passed. */ + prebidGlobal?: string; + /** + * Analytics behavior forwarded to the `OptablePrebidAnalytics` constructor + * (tenant, samplingRate, debug, getSplitTestAssignment, …). + */ + analytics?: OptablePrebidAnalyticsConfig; +} + +/** + * Bootstrap Prebid.js analytics for a bundle: create a dedicated read-only + * analytics SDK instance, construct an `OptablePrebidAnalytics`, and hook it + * into the publisher's Prebid.js global. + * + * Returns the analytics instance, or `null` when no Prebid.js global is present + * (in which case no analytics SDK instance is created). + * @param options - See {@link InitPrebidAnalyticsOptions}. + */ +export function initPrebidAnalytics(options: InitPrebidAnalyticsOptions): OptablePrebidAnalytics | null { + const { SDK, instance, pbjs, prebidGlobal, analytics: analyticsConfig } = options; + + const prebid = pbjs ?? (window as any)[prebidGlobal || "pbjs"]; + if (!prebid) return null; + + const analyticsSDK = new SDK({ + readOnly: true, + cookies: false, + ...instance, + }); + + const analytics = new OptablePrebidAnalytics(analyticsSDK, { + analytics: true, + ...analyticsConfig, + }); + + analytics.hookIntoPrebid(prebid); + return analytics; +}