Skip to content

Commit

Permalink
feat: Added impression and quartile tracking
Browse files Browse the repository at this point in the history
  • Loading branch information
matvp91 committed Jan 3, 2025
1 parent 34fe0a7 commit b4ee1cf
Show file tree
Hide file tree
Showing 5 changed files with 104 additions and 82 deletions.
78 changes: 4 additions & 74 deletions packages/stitcher/src/playlist.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,12 @@ import {
stringifyMediaPlaylist,
} from "./parser";
import { updateSession } from "./session";
import { getSignalingForAsset } from "./signaling";
import { fetchVmap, toAdBreakTimeOffset } from "./vmap";
import type { Filter } from "./filters";
import type { MasterPlaylist, MediaPlaylist } from "./parser";
import type { Session } from "./session";
import type { Interstitial, InterstitialAsset } from "./types";
import type { Interstitial } from "./types";
import type { VmapAdBreak } from "./vmap";
import type { DateTime } from "luxon";

Expand Down Expand Up @@ -76,21 +77,13 @@ export async function formatMediaPlaylist(
export async function formatAssetList(session: Session, dateTime: DateTime) {
const assets = await getAssets(session, dateTime);

await Promise.all(
assets.map(async (asset) => {
if (asset.duration === undefined) {
asset.duration = await fetchDuration(asset.url);
}
}),
);

return {
ASSETS: assets.map((asset) => {
return {
URI: asset.url,
DURATION: asset.duration,
"SPRS-KIND": asset.kind,
"X-AD-CREATIVE-SIGNALING": getAdCreativeSignaling(assets, asset),
"X-AD-CREATIVE-SIGNALING": getSignalingForAsset(assets, asset),
};
}),
};
Expand All @@ -108,8 +101,7 @@ async function fetchMediaPlaylist(url: string) {
return parseMediaPlaylist(result);
}

export async function fetchDuration(uri: string) {
const url = resolveUri(uri);
export async function fetchDuration(url: string) {
const variant = (await fetchMasterPlaylist(url))?.variants[0];

if (!variant) {
Expand Down Expand Up @@ -249,65 +241,3 @@ export function mapAdBreaksToSessionInterstitials(

return interstitials;
}

interface SignalingEvent {
type: "clickthrough" | "quartile";
start?: number;
urls: string[];
}

export function getAdCreativeSignaling(
assets: InterstitialAsset[],
asset: InterstitialAsset,
) {
const { duration, tracking } = asset;

assert(duration);

if (!tracking) {
return null;
}

let startTime = 0;
for (const tempAsset of assets) {
if (tempAsset === asset) {
break;
}
assert(tempAsset.duration);
startTime += tempAsset.duration;
}

const signalingEvents: SignalingEvent[] = [];

Object.entries(tracking).forEach(([name, urls]) => {
const offset = QUARTILE_EVENTS[name];
if (offset !== undefined) {
signalingEvents.push({
type: "quartile",
start: startTime + duration * offset,
urls,
});
}
});

return {
version: 2,
type: "slot",
payload: [
{
type: "linear",
start: startTime,
duration: asset.duration,
tracking: signalingEvents,
},
],
};
}

const QUARTILE_EVENTS: Record<string, number> = {
start: 0,
firstQuartile: 0.25,
midpoint: 0.5,
thirdQuartile: 0.75,
complete: 1,
};
15 changes: 10 additions & 5 deletions packages/stitcher/src/session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { kv } from "./adapters/kv";
import { mergeInterstitials } from "./interstitials";
import { JSON } from "./lib/json";
import { resolveUri } from "./lib/url";
import { fetchDuration } from "./playlist";
import type { Interstitial, InterstitialChunk } from "./types";
import type { VmapParams } from "./vmap";

Expand Down Expand Up @@ -53,7 +54,7 @@ export async function createSession(params: {
};

if (params.interstitials) {
const interstitials = mapSessionInterstitials(
const interstitials = await mapSessionInterstitials(
startTime,
params.interstitials,
);
Expand All @@ -79,7 +80,7 @@ export async function updateSession(session: Session) {
await kv.set(`session:${session.id}`, value, session.expiry);
}

function mapSessionInterstitials(
async function mapSessionInterstitials(
startTime: DateTime,
interstitials: SessionInterstitial[],
) {
Expand All @@ -95,12 +96,16 @@ function mapSessionInterstitials(

if (interstitial.assets) {
for (const asset of interstitial.assets) {
const { uri, ...rest } = asset;
const { uri, kind } = asset;

const url = resolveUri(uri);

chunks.push({
type: "asset",
data: {
url: resolveUri(uri),
...rest,
url,
duration: await fetchDuration(url),
kind,
},
});
}
Expand Down
69 changes: 69 additions & 0 deletions packages/stitcher/src/signaling.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import type { InterstitialAsset } from "./types";

interface SignalingEvent {
type: "impression" | "quartile" | "clickthrough";
start?: number;
urls: string[];
}

const QUARTILE_EVENTS: Record<string, number> = {
start: 0,
firstQuartile: 0.25,
midpoint: 0.5,
thirdQuartile: 0.75,
complete: 1,
};

export function getSignalingForAsset(
assets: InterstitialAsset[],
asset: InterstitialAsset,
) {
const { duration, tracking } = asset;
if (!tracking) {
return null;
}

const assetIndex = assets.indexOf(asset);
const startTime = assets.splice(0, assetIndex).reduce((acc, asset) => {
acc += asset.duration;
return acc;
}, 0);

const signalingEvents: SignalingEvent[] = [];

signalingEvents.push({
type: "impression",
start: 0,
urls: tracking.impression,
});

signalingEvents.push({
type: "clickthrough",
urls: tracking.clickThrough,
});

// Map each tracking URL to their corresponding quartile.
Object.entries(tracking).forEach(([name, urls]) => {
const offset = QUARTILE_EVENTS[name];
if (offset !== undefined) {
signalingEvents.push({
type: "quartile",
start: duration * offset,
urls,
});
}
});

return {
version: 2,
type: "slot",
payload: [
{
type: "linear",
start: startTime,
duration: asset.duration,
tracking: signalingEvents,
},
],
};
}
8 changes: 6 additions & 2 deletions packages/stitcher/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,13 @@ export interface InterstitialVast {

export interface InterstitialAsset {
url: string;
duration?: number;
duration: number;
kind?: "ad" | "bumper";
tracking?: Record<string, string[]>;
tracking?: {
impression: string[];
clickThrough: string[];
[key: string]: string[];
};
}

export interface InterstitialAssetList {
Expand Down
16 changes: 15 additions & 1 deletion packages/stitcher/src/vast.ts
Original file line number Diff line number Diff line change
Expand Up @@ -111,11 +111,25 @@ async function mapAdToAsset(ad: VastAd): Promise<InterstitialAsset | null> {
return null;
}

const impressionUrls: string[] = [];
for (const urlTemplate of ad.impressionURLTemplates) {
impressionUrls.push(urlTemplate.url);
}

const clickThroughUrls: string[] = [];
for (const urlTemplate of creative.videoClickTrackingURLTemplates) {
clickThroughUrls.push(urlTemplate.url);
}

return {
url: url,
duration: creative.duration,
kind: "ad",
tracking: creative.trackingEvents,
tracking: {
impression: impressionUrls,
clickThrough: clickThroughUrls,
...creative.trackingEvents,
},
};
}

Expand Down

0 comments on commit b4ee1cf

Please sign in to comment.