Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
127 changes: 86 additions & 41 deletions lib/addons/prebid/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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**:
Expand Down
102 changes: 101 additions & 1 deletion lib/addons/prebid/analytics.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import OptablePrebidAnalytics from "./analytics";
import OptablePrebidAnalytics, { initPrebidAnalytics } from "./analytics";
import type OptableSDK from "../../sdk";

// Mock the SDK_WRAPPER_VERSION global
Expand Down Expand Up @@ -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 }));
});
});
54 changes: 54 additions & 0 deletions lib/addons/prebid/analytics.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";

Expand Down Expand Up @@ -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;
}