gtm-datalayer Component
gtm-datalayer is a Surface Workflow component that pushes MonetizationOS (MOS) decision and
visitor data to the client’s analytics data layer. It runs as a component workflow in parallel
with the other components
(article-body,
demeter), consuming the properties.outcome,
properties.visitor, and properties.logs produced by the parent Surface Workflow (see
Website Surface Workflow) — it makes no decisions of
its own.
Like all Surface Workflow components, gtm-datalayer is authored and configured inside MOS (the
surface editor) per client, not part of the Cloudflare Worker repository. The code below is the
reference implementation a client copies in and adapts.
Responsibilities
- Push
visitorType,decision_source, andis_sophi_script_blockedto the data layer for downstream analytics on every page. - Push a
sophi_wall_shownevent, with the wall type, when a paywall or regwall is displayed (outcome.wallVisibility === 'always'andoutcome.wallType !== 'none').
Output
A before content element: a single <script> tag prepended to the page that performs the
data-layer pushes described above. Nothing is rendered when debug logging is off.
Extension Points
Configurable data-layer target
- Purpose: let a client push to whatever data layer object/method their analytics stack
actually uses, instead of assuming Google Tag Manager’s
window.dataLayer. - Input: a client-defined data-layer variable name and push pattern (e.g. a different global,
or a function call instead of an array
push). - Default starting point: the reference implementation below always targets the standard GTM
pattern —
window.dataLayer = window.dataLayer || []; window.dataLayer.push(event...). - Status: no default abstraction is provided yet — a client that needs a non-GTM data layer
replaces
dataLayerScriptbelow directly.
Debug Logging
When env.ENABLE_LOGGING === 'debug', two extra console.log elements are emitted before the
data-layer script: one summarizing this component’s own inputs/outputs
(surfaceProperties.visitor and surfaceProperties.outcome, plus the wallShown flag and what
the push will contain), and one dumping the parent Surface Workflow’s full properties.logs
decision trail, so the two can be cross-referenced in the browser console.
Reference Implementation
// ─── GTM DataLayer Component ──────────────────────────────────────────────────
//
// Manages interactions with the Google Tag Manager dataLayer. Runs as a
// component workflow in parallel with other components (article-body, demeter).
//
// Responsibilities:
// - Push visitorType, decision_source, and is_sophi_script_blocked to the dataLayer
// for downstream analytics
// - Push a sophi_wall_shown event when a paywall or regwall is displayed
type WebElement = { type: 'html'; content: string };
const workflow: SurfaceWorkflow = async ({ surfaceProperties, env }) => {
const debug = env.ENABLE_LOGGING === 'debug';
// ─── Read visitor info from Surface Workflow ──────────────────────────────
const visitor = (surfaceProperties?.visitor ?? {}) as Record<string, unknown>;
const visitorType = String(visitor.visitorType ?? 'anonymous');
// Upstream workflow emits a real boolean; narrow explicitly since visitor is untyped here
const isSophiScriptBlocked = visitor.isSophiScriptBlocked === true
// ─── Read the Surface Workflow's access decision ──────────────────────────
const outcome = (surfaceProperties?.outcome ?? {}) as Record<string, unknown>;
const wallVisibility = String(outcome.wallVisibility ?? 'never');
const wallType = String(outcome.wallType ?? 'none');
const decisionSource = String(outcome.decisionSource ?? 'override');
const wallShown = wallVisibility === 'always' && wallType !== 'none';
const elements: WebElement[] = [];
if (debug) {
const payload = JSON.stringify({
component: 'gtm-datalayer',
surfaceProperties: {
visitor: { visitorType, isSophiScriptBlocked },
outcome: { wallVisibility, wallType, decisionSource },
},
flags: { wallShown },
output: `dataLayer.push with visitorType + decision_source + is_sophi_script_blocked${wallShown ? ' + sophi_wall_shown event' : ''}`,
});
elements.push({ type: 'html', content: `<script>console.log("[MOS Debug] gtm-datalayer", ${payload});</script>` });
// Surface Workflow's full decision trail (meter values, cookie state, path taken)
const logsPayload = JSON.stringify(surfaceProperties?.logs ?? {});
elements.push({ type: 'html', content: `<script>console.log("[MOS Debug] surfaceProperties.logs", ${logsPayload});</script>` });
}
elements.push(dataLayerScript(visitorType, decisionSource, isSophiScriptBlocked, wallShown ? wallType : undefined));
return {
content: {
before: elements,
},
};
};
// ─── Helpers ──────────────────────────────────────────────────────────────────
/**
* Builds a `<script>` element that pushes visitor metadata to the GTM dataLayer.
* Includes decision_source and is_sophi_script_blocked for observability.
* When a wall is shown, also pushes a `sophi_wall_shown` event with the wall type.
*/
function dataLayerScript(visitorType: string, decisionSource: string, isSophiScriptBlocked: boolean, wallType?: string): WebElement {
const wallPush = wallType
? `\nwindow.dataLayer.push(${JSON.stringify({ event: 'sophi_wall_shown', wall_type: wallType })});`
: '';
return {
type: 'html',
content: `<script>
window.dataLayer = window.dataLayer || [];
window.dataLayer.push(${JSON.stringify({ visitorType, decision_source: decisionSource, is_sophi_script_blocked: isSophiScriptBlocked })});${wallPush}
</script>`,
};
}
export default workflow;