@otl-core/analytics 1.0.0 → 1.0.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs ADDED
@@ -0,0 +1,286 @@
1
+ "use client";
2
+ 'use strict';
3
+
4
+ var react = require('react');
5
+ var jsxRuntime = require('react/jsx-runtime');
6
+
7
+ // src/data-layer.ts
8
+ var CMSDataLayer = class {
9
+ constructor() {
10
+ this.entries = [];
11
+ this.subscribers = /* @__PURE__ */ new Set();
12
+ }
13
+ push(entry) {
14
+ this.entries.push(entry);
15
+ for (const fn of this.subscribers) {
16
+ try {
17
+ fn(entry);
18
+ } catch {
19
+ }
20
+ }
21
+ }
22
+ subscribe(fn) {
23
+ for (const entry of this.entries) {
24
+ try {
25
+ fn(entry);
26
+ } catch {
27
+ }
28
+ }
29
+ this.subscribers.add(fn);
30
+ return () => {
31
+ this.subscribers.delete(fn);
32
+ };
33
+ }
34
+ };
35
+ var dataLayer = new CMSDataLayer();
36
+ if (typeof window !== "undefined") {
37
+ window.__cms = dataLayer;
38
+ }
39
+ var AnalyticsContext = react.createContext({
40
+ trackEvent: () => {
41
+ },
42
+ enabled: false,
43
+ abnVariant: null,
44
+ setABnVariant: () => {
45
+ },
46
+ globalFormTracking: false
47
+ });
48
+ function useAnalytics() {
49
+ return react.useContext(AnalyticsContext);
50
+ }
51
+ function AnalyticsProvider({
52
+ enabled = true,
53
+ globalFormTracking = false,
54
+ children
55
+ }) {
56
+ const [abnVariant, setABnVariant] = react.useState(null);
57
+ const trackEvent = react.useCallback(
58
+ (event, params) => {
59
+ if (!enabled) return;
60
+ const variantParams = {};
61
+ if (abnVariant) {
62
+ variantParams.abn_variant_id = abnVariant.variantId;
63
+ if (abnVariant.experimentId) {
64
+ variantParams.abn_experiment_id = abnVariant.experimentId;
65
+ }
66
+ }
67
+ dataLayer.push({
68
+ event,
69
+ timestamp: Date.now(),
70
+ ...variantParams,
71
+ ...params
72
+ });
73
+ },
74
+ [enabled, abnVariant]
75
+ );
76
+ const value = react.useMemo(
77
+ () => ({
78
+ trackEvent,
79
+ enabled,
80
+ abnVariant,
81
+ setABnVariant,
82
+ globalFormTracking
83
+ }),
84
+ [trackEvent, enabled, abnVariant, globalFormTracking]
85
+ );
86
+ return /* @__PURE__ */ jsxRuntime.jsx(AnalyticsContext.Provider, { value, children });
87
+ }
88
+ function BlockAnalyticsWrapper({
89
+ analyticsConfig,
90
+ blockId,
91
+ blockType,
92
+ children
93
+ }) {
94
+ const wrapperRef = react.useRef(null);
95
+ if (!analyticsConfig || !analyticsConfig.enabled) {
96
+ return /* @__PURE__ */ jsxRuntime.jsx(jsxRuntime.Fragment, { children });
97
+ }
98
+ return /* @__PURE__ */ jsxRuntime.jsx(
99
+ BlockAnalyticsInner,
100
+ {
101
+ analyticsConfig,
102
+ blockId,
103
+ blockType,
104
+ wrapperRef,
105
+ children
106
+ }
107
+ );
108
+ }
109
+ function BlockAnalyticsInner({
110
+ analyticsConfig,
111
+ blockId,
112
+ blockType,
113
+ wrapperRef,
114
+ children
115
+ }) {
116
+ const {
117
+ event_label,
118
+ track_type,
119
+ visibility_threshold = 50,
120
+ fire_once = true,
121
+ target_providers = "all",
122
+ custom_params = {}
123
+ } = analyticsConfig;
124
+ const baseParams = {
125
+ block_id: blockId,
126
+ block_type: blockType,
127
+ event_label,
128
+ ...custom_params,
129
+ ...target_providers !== "all" ? { _target_providers: target_providers } : {}
130
+ };
131
+ const handleClick = react.useCallback(() => {
132
+ if (track_type === "click" || track_type === "both") {
133
+ dataLayer.push({
134
+ event: "block_click",
135
+ timestamp: Date.now(),
136
+ ...baseParams
137
+ });
138
+ }
139
+ }, [track_type, blockId, blockType, event_label]);
140
+ react.useEffect(() => {
141
+ if (track_type !== "visibility" && track_type !== "both") return;
142
+ if (!wrapperRef.current) return;
143
+ const threshold = Math.min(Math.max(visibility_threshold, 0), 100) / 100;
144
+ const observer = new IntersectionObserver(
145
+ (entries) => {
146
+ for (const entry of entries) {
147
+ if (entry.isIntersecting) {
148
+ dataLayer.push({
149
+ event: "block_visible",
150
+ timestamp: Date.now(),
151
+ ...baseParams
152
+ });
153
+ if (fire_once) {
154
+ observer.disconnect();
155
+ }
156
+ }
157
+ }
158
+ },
159
+ { threshold }
160
+ );
161
+ observer.observe(wrapperRef.current);
162
+ return () => {
163
+ observer.disconnect();
164
+ };
165
+ }, [
166
+ track_type,
167
+ visibility_threshold,
168
+ fire_once,
169
+ blockId,
170
+ blockType,
171
+ event_label
172
+ ]);
173
+ return /* @__PURE__ */ jsxRuntime.jsx("div", { ref: wrapperRef, onClick: handleClick, children });
174
+ }
175
+ function SectionAnalyticsWrapper({
176
+ analyticsConfig,
177
+ sectionId,
178
+ sectionType,
179
+ children
180
+ }) {
181
+ const wrapperRef = react.useRef(null);
182
+ if (!analyticsConfig || !analyticsConfig.enabled) {
183
+ return /* @__PURE__ */ jsxRuntime.jsx(jsxRuntime.Fragment, { children });
184
+ }
185
+ return /* @__PURE__ */ jsxRuntime.jsx(
186
+ SectionAnalyticsInner,
187
+ {
188
+ analyticsConfig,
189
+ sectionId,
190
+ sectionType,
191
+ wrapperRef,
192
+ children
193
+ }
194
+ );
195
+ }
196
+ function SectionAnalyticsInner({
197
+ analyticsConfig,
198
+ sectionId,
199
+ sectionType,
200
+ wrapperRef,
201
+ children
202
+ }) {
203
+ const {
204
+ event_label,
205
+ track_type,
206
+ visibility_threshold = 50,
207
+ fire_once = true,
208
+ target_providers = "all",
209
+ custom_params = {}
210
+ } = analyticsConfig;
211
+ const baseParams = {
212
+ section_id: sectionId,
213
+ section_type: sectionType,
214
+ event_label,
215
+ ...custom_params,
216
+ ...target_providers !== "all" ? { _target_providers: target_providers } : {}
217
+ };
218
+ const handleClick = react.useCallback(() => {
219
+ if (track_type === "click" || track_type === "both") {
220
+ dataLayer.push({
221
+ event: "block_click",
222
+ timestamp: Date.now(),
223
+ ...baseParams
224
+ });
225
+ }
226
+ }, [track_type, sectionId, sectionType, event_label]);
227
+ react.useEffect(() => {
228
+ if (track_type !== "visibility" && track_type !== "both") return;
229
+ if (!wrapperRef.current) return;
230
+ const threshold = Math.min(Math.max(visibility_threshold, 0), 100) / 100;
231
+ const observer = new IntersectionObserver(
232
+ (entries) => {
233
+ for (const entry of entries) {
234
+ if (entry.isIntersecting) {
235
+ dataLayer.push({
236
+ event: "block_visible",
237
+ timestamp: Date.now(),
238
+ ...baseParams
239
+ });
240
+ if (fire_once) {
241
+ observer.disconnect();
242
+ }
243
+ }
244
+ }
245
+ },
246
+ { threshold }
247
+ );
248
+ observer.observe(wrapperRef.current);
249
+ return () => {
250
+ observer.disconnect();
251
+ };
252
+ }, [
253
+ track_type,
254
+ visibility_threshold,
255
+ fire_once,
256
+ sectionId,
257
+ sectionType,
258
+ event_label
259
+ ]);
260
+ return /* @__PURE__ */ jsxRuntime.jsx("div", { ref: wrapperRef, onClick: handleClick, children });
261
+ }
262
+ function ABnVariantSetter({
263
+ variantId,
264
+ experimentId
265
+ }) {
266
+ const { setABnVariant, trackEvent } = useAnalytics();
267
+ react.useEffect(() => {
268
+ setABnVariant({ variantId, experimentId });
269
+ trackEvent("experiment_view", {
270
+ abn_variant_id: variantId,
271
+ abn_experiment_id: experimentId
272
+ });
273
+ return () => setABnVariant(null);
274
+ }, [variantId, experimentId, setABnVariant, trackEvent]);
275
+ return null;
276
+ }
277
+
278
+ exports.ABnVariantSetter = ABnVariantSetter;
279
+ exports.AnalyticsProvider = AnalyticsProvider;
280
+ exports.BlockAnalyticsWrapper = BlockAnalyticsWrapper;
281
+ exports.CMSDataLayer = CMSDataLayer;
282
+ exports.SectionAnalyticsWrapper = SectionAnalyticsWrapper;
283
+ exports.dataLayer = dataLayer;
284
+ exports.useAnalytics = useAnalytics;
285
+ //# sourceMappingURL=index.cjs.map
286
+ //# sourceMappingURL=index.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/data-layer.ts","../src/analytics-context.tsx","../src/block-analytics-wrapper.tsx","../src/section-analytics-wrapper.tsx","../src/abn-variant-setter.tsx"],"names":["createContext","useContext","useState","useCallback","useMemo","jsx","useRef","Fragment","useEffect"],"mappings":"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","file":"index.cjs","sourcesContent":["/**\n * CMS Data Layer\n *\n * Central event bus for analytics events. All events flow through here.\n * Provider adapters subscribe and translate events to provider-specific calls.\n */\n\nimport type { DataLayerEntry, DataLayerSubscriber } from \"./types\";\n\ndeclare global {\n interface Window {\n __cms?: CMSDataLayer;\n }\n}\n\nexport class CMSDataLayer {\n readonly entries: DataLayerEntry[] = [];\n private readonly subscribers = new Set<DataLayerSubscriber>();\n\n push(entry: DataLayerEntry): void {\n this.entries.push(entry);\n for (const fn of this.subscribers) {\n try {\n fn(entry);\n } catch {\n // Swallow subscriber errors to prevent cascade failures\n }\n }\n }\n\n subscribe(fn: DataLayerSubscriber): () => void {\n for (const entry of this.entries) {\n try {\n fn(entry);\n } catch {\n // Swallow replay errors\n }\n }\n this.subscribers.add(fn);\n return () => {\n this.subscribers.delete(fn);\n };\n }\n}\n\nexport const dataLayer = new CMSDataLayer();\n\nif (typeof window !== \"undefined\") {\n window.__cms = dataLayer;\n}\n","\"use client\";\n\nimport {\n createContext,\n useCallback,\n useContext,\n useMemo,\n useState,\n} from \"react\";\nimport type { StandardEventName } from \"@otl-core/cms-types\";\nimport type { ABnVariantContext, AnalyticsContextValue } from \"./types\";\nimport { dataLayer } from \"./data-layer\";\n\nconst AnalyticsContext = createContext<AnalyticsContextValue>({\n trackEvent: () => {},\n enabled: false,\n abnVariant: null,\n setABnVariant: () => {},\n globalFormTracking: false,\n});\n\nexport function useAnalytics(): AnalyticsContextValue {\n return useContext(AnalyticsContext);\n}\n\ninterface AnalyticsProviderProps {\n enabled?: boolean;\n /** When true, all forms automatically emit form_start, form_submit, form_error. */\n globalFormTracking?: boolean;\n children: React.ReactNode;\n}\n\nexport function AnalyticsProvider({\n enabled = true,\n globalFormTracking = false,\n children,\n}: AnalyticsProviderProps) {\n const [abnVariant, setABnVariant] = useState<ABnVariantContext | null>(null);\n\n const trackEvent = useCallback(\n (event: StandardEventName, params?: Record<string, unknown>) => {\n if (!enabled) return;\n const variantParams: Record<string, unknown> = {};\n if (abnVariant) {\n variantParams.abn_variant_id = abnVariant.variantId;\n if (abnVariant.experimentId) {\n variantParams.abn_experiment_id = abnVariant.experimentId;\n }\n }\n dataLayer.push({\n event,\n timestamp: Date.now(),\n ...variantParams,\n ...params,\n });\n },\n [enabled, abnVariant]\n );\n\n const value = useMemo<AnalyticsContextValue>(\n () => ({\n trackEvent,\n enabled,\n abnVariant,\n setABnVariant,\n globalFormTracking,\n }),\n [trackEvent, enabled, abnVariant, globalFormTracking]\n );\n\n return (\n <AnalyticsContext.Provider value={value}>\n {children}\n </AnalyticsContext.Provider>\n );\n}\n","\"use client\";\n\n/**\n * Block Analytics Wrapper\n *\n * Wraps a block and adds click/visibility tracking based on BlockAnalyticsConfig.\n */\n\nimport { useCallback, useEffect, useRef } from \"react\";\nimport type { BlockAnalyticsConfig } from \"@otl-core/cms-types\";\nimport { dataLayer } from \"./data-layer\";\n\ninterface BlockAnalyticsWrapperProps {\n analyticsConfig: BlockAnalyticsConfig | undefined;\n blockId: string;\n blockType: string;\n children: React.ReactNode;\n}\n\nexport function BlockAnalyticsWrapper({\n analyticsConfig,\n blockId,\n blockType,\n children,\n}: BlockAnalyticsWrapperProps) {\n const wrapperRef = useRef<HTMLDivElement>(null);\n\n if (!analyticsConfig || !analyticsConfig.enabled) {\n return <>{children}</>;\n }\n\n return (\n <BlockAnalyticsInner\n analyticsConfig={analyticsConfig}\n blockId={blockId}\n blockType={blockType}\n wrapperRef={wrapperRef}\n >\n {children}\n </BlockAnalyticsInner>\n );\n}\n\ninterface BlockAnalyticsInnerProps {\n analyticsConfig: BlockAnalyticsConfig;\n blockId: string;\n blockType: string;\n wrapperRef: React.RefObject<HTMLDivElement | null>;\n children: React.ReactNode;\n}\n\nfunction BlockAnalyticsInner({\n analyticsConfig,\n blockId,\n blockType,\n wrapperRef,\n children,\n}: BlockAnalyticsInnerProps) {\n const {\n event_label,\n track_type,\n visibility_threshold = 50,\n fire_once = true,\n target_providers = \"all\",\n custom_params = {},\n } = analyticsConfig;\n\n const baseParams = {\n block_id: blockId,\n block_type: blockType,\n event_label,\n ...custom_params,\n ...(target_providers !== \"all\"\n ? { _target_providers: target_providers }\n : {}),\n };\n\n const handleClick = useCallback(() => {\n if (track_type === \"click\" || track_type === \"both\") {\n dataLayer.push({\n event: \"block_click\",\n timestamp: Date.now(),\n ...baseParams,\n });\n }\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [track_type, blockId, blockType, event_label]);\n\n useEffect(() => {\n if (track_type !== \"visibility\" && track_type !== \"both\") return;\n if (!wrapperRef.current) return;\n\n const threshold = Math.min(Math.max(visibility_threshold, 0), 100) / 100;\n const observer = new IntersectionObserver(\n entries => {\n for (const entry of entries) {\n if (entry.isIntersecting) {\n dataLayer.push({\n event: \"block_visible\",\n timestamp: Date.now(),\n ...baseParams,\n });\n\n if (fire_once) {\n observer.disconnect();\n }\n }\n }\n },\n { threshold }\n );\n\n observer.observe(wrapperRef.current);\n\n return () => {\n observer.disconnect();\n };\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [\n track_type,\n visibility_threshold,\n fire_once,\n blockId,\n blockType,\n event_label,\n ]);\n\n return (\n <div ref={wrapperRef} onClick={handleClick}>\n {children}\n </div>\n );\n}\n","\"use client\";\n\n/**\n * Section Analytics Wrapper\n *\n * Wraps a section and adds click/visibility tracking based on BlockAnalyticsConfig.\n */\n\nimport { useCallback, useEffect, useRef } from \"react\";\nimport type { BlockAnalyticsConfig } from \"@otl-core/cms-types\";\nimport { dataLayer } from \"./data-layer\";\n\ninterface SectionAnalyticsWrapperProps {\n analyticsConfig: BlockAnalyticsConfig | undefined;\n sectionId: string;\n sectionType: string;\n children: React.ReactNode;\n}\n\nexport function SectionAnalyticsWrapper({\n analyticsConfig,\n sectionId,\n sectionType,\n children,\n}: SectionAnalyticsWrapperProps) {\n const wrapperRef = useRef<HTMLDivElement>(null);\n\n if (!analyticsConfig || !analyticsConfig.enabled) {\n return <>{children}</>;\n }\n\n return (\n <SectionAnalyticsInner\n analyticsConfig={analyticsConfig}\n sectionId={sectionId}\n sectionType={sectionType}\n wrapperRef={wrapperRef}\n >\n {children}\n </SectionAnalyticsInner>\n );\n}\n\ninterface SectionAnalyticsInnerProps {\n analyticsConfig: BlockAnalyticsConfig;\n sectionId: string;\n sectionType: string;\n wrapperRef: React.RefObject<HTMLDivElement | null>;\n children: React.ReactNode;\n}\n\nfunction SectionAnalyticsInner({\n analyticsConfig,\n sectionId,\n sectionType,\n wrapperRef,\n children,\n}: SectionAnalyticsInnerProps) {\n const {\n event_label,\n track_type,\n visibility_threshold = 50,\n fire_once = true,\n target_providers = \"all\",\n custom_params = {},\n } = analyticsConfig;\n\n const baseParams = {\n section_id: sectionId,\n section_type: sectionType,\n event_label,\n ...custom_params,\n ...(target_providers !== \"all\"\n ? { _target_providers: target_providers }\n : {}),\n };\n\n const handleClick = useCallback(() => {\n if (track_type === \"click\" || track_type === \"both\") {\n dataLayer.push({\n event: \"block_click\",\n timestamp: Date.now(),\n ...baseParams,\n });\n }\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [track_type, sectionId, sectionType, event_label]);\n\n useEffect(() => {\n if (track_type !== \"visibility\" && track_type !== \"both\") return;\n if (!wrapperRef.current) return;\n\n const threshold = Math.min(Math.max(visibility_threshold, 0), 100) / 100;\n const observer = new IntersectionObserver(\n entries => {\n for (const entry of entries) {\n if (entry.isIntersecting) {\n dataLayer.push({\n event: \"block_visible\",\n timestamp: Date.now(),\n ...baseParams,\n });\n\n if (fire_once) {\n observer.disconnect();\n }\n }\n }\n },\n { threshold }\n );\n\n observer.observe(wrapperRef.current);\n\n return () => {\n observer.disconnect();\n };\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [\n track_type,\n visibility_threshold,\n fire_once,\n sectionId,\n sectionType,\n event_label,\n ]);\n\n return (\n <div ref={wrapperRef} onClick={handleClick}>\n {children}\n </div>\n );\n}\n","\"use client\";\n\nimport { useEffect } from \"react\";\nimport type { StandardEventName } from \"@otl-core/cms-types\";\nimport { useAnalytics } from \"./analytics-context\";\n\ninterface ABnVariantSetterProps {\n variantId: string;\n experimentId?: string;\n}\n\n/**\n * A client component that page components render to communicate the\n * resolved A/B test variant to the analytics context.\n *\n * Renders nothing -- just sets context on mount and emits experiment_view.\n */\nexport function ABnVariantSetter({\n variantId,\n experimentId,\n}: ABnVariantSetterProps) {\n const { setABnVariant, trackEvent } = useAnalytics();\n\n useEffect(() => {\n setABnVariant({ variantId, experimentId });\n trackEvent(\"experiment_view\" as StandardEventName, {\n abn_variant_id: variantId,\n abn_experiment_id: experimentId,\n });\n return () => setABnVariant(null);\n }, [variantId, experimentId, setABnVariant, trackEvent]);\n\n return null;\n}\n"]}
@@ -0,0 +1,86 @@
1
+ import { StandardEventName, BlockAnalyticsConfig } from '@otl-core/cms-types';
2
+ import * as react_jsx_runtime from 'react/jsx-runtime';
3
+
4
+ interface DataLayerEntry {
5
+ event: StandardEventName;
6
+ timestamp: number;
7
+ [key: string]: unknown;
8
+ }
9
+ type DataLayerSubscriber = (entry: DataLayerEntry) => void;
10
+ interface ABnVariantContext {
11
+ /** The variant ID the user was assigned to. */
12
+ variantId: string;
13
+ /** A human-readable experiment name or the content ID being tested. */
14
+ experimentId?: string;
15
+ }
16
+ interface AnalyticsContextValue {
17
+ /** Push an event to the data layer. Variant context (if set) is automatically merged. */
18
+ trackEvent(event: StandardEventName, params?: Record<string, unknown>): void;
19
+ /** Whether analytics is active (false in preview/draft mode). */
20
+ enabled: boolean;
21
+ /** The current A/B test variant context, if any. */
22
+ abnVariant: ABnVariantContext | null;
23
+ /** Set the A/B variant context. Called by ABnVariantSetter from page components. */
24
+ setABnVariant(variant: ABnVariantContext | null): void;
25
+ /** When true, ALL forms automatically track form_start, form_submit, and form_error regardless of per-form settings. */
26
+ globalFormTracking: boolean;
27
+ }
28
+
29
+ /**
30
+ * CMS Data Layer
31
+ *
32
+ * Central event bus for analytics events. All events flow through here.
33
+ * Provider adapters subscribe and translate events to provider-specific calls.
34
+ */
35
+
36
+ declare global {
37
+ interface Window {
38
+ __cms?: CMSDataLayer;
39
+ }
40
+ }
41
+ declare class CMSDataLayer {
42
+ readonly entries: DataLayerEntry[];
43
+ private readonly subscribers;
44
+ push(entry: DataLayerEntry): void;
45
+ subscribe(fn: DataLayerSubscriber): () => void;
46
+ }
47
+ declare const dataLayer: CMSDataLayer;
48
+
49
+ declare function useAnalytics(): AnalyticsContextValue;
50
+ interface AnalyticsProviderProps {
51
+ enabled?: boolean;
52
+ /** When true, all forms automatically emit form_start, form_submit, form_error. */
53
+ globalFormTracking?: boolean;
54
+ children: React.ReactNode;
55
+ }
56
+ declare function AnalyticsProvider({ enabled, globalFormTracking, children, }: AnalyticsProviderProps): react_jsx_runtime.JSX.Element;
57
+
58
+ interface BlockAnalyticsWrapperProps {
59
+ analyticsConfig: BlockAnalyticsConfig | undefined;
60
+ blockId: string;
61
+ blockType: string;
62
+ children: React.ReactNode;
63
+ }
64
+ declare function BlockAnalyticsWrapper({ analyticsConfig, blockId, blockType, children, }: BlockAnalyticsWrapperProps): react_jsx_runtime.JSX.Element;
65
+
66
+ interface SectionAnalyticsWrapperProps {
67
+ analyticsConfig: BlockAnalyticsConfig | undefined;
68
+ sectionId: string;
69
+ sectionType: string;
70
+ children: React.ReactNode;
71
+ }
72
+ declare function SectionAnalyticsWrapper({ analyticsConfig, sectionId, sectionType, children, }: SectionAnalyticsWrapperProps): react_jsx_runtime.JSX.Element;
73
+
74
+ interface ABnVariantSetterProps {
75
+ variantId: string;
76
+ experimentId?: string;
77
+ }
78
+ /**
79
+ * A client component that page components render to communicate the
80
+ * resolved A/B test variant to the analytics context.
81
+ *
82
+ * Renders nothing -- just sets context on mount and emits experiment_view.
83
+ */
84
+ declare function ABnVariantSetter({ variantId, experimentId, }: ABnVariantSetterProps): null;
85
+
86
+ export { type ABnVariantContext, ABnVariantSetter, type AnalyticsContextValue, AnalyticsProvider, BlockAnalyticsWrapper, CMSDataLayer, type DataLayerEntry, type DataLayerSubscriber, SectionAnalyticsWrapper, dataLayer, useAnalytics };
@@ -0,0 +1,86 @@
1
+ import { StandardEventName, BlockAnalyticsConfig } from '@otl-core/cms-types';
2
+ import * as react_jsx_runtime from 'react/jsx-runtime';
3
+
4
+ interface DataLayerEntry {
5
+ event: StandardEventName;
6
+ timestamp: number;
7
+ [key: string]: unknown;
8
+ }
9
+ type DataLayerSubscriber = (entry: DataLayerEntry) => void;
10
+ interface ABnVariantContext {
11
+ /** The variant ID the user was assigned to. */
12
+ variantId: string;
13
+ /** A human-readable experiment name or the content ID being tested. */
14
+ experimentId?: string;
15
+ }
16
+ interface AnalyticsContextValue {
17
+ /** Push an event to the data layer. Variant context (if set) is automatically merged. */
18
+ trackEvent(event: StandardEventName, params?: Record<string, unknown>): void;
19
+ /** Whether analytics is active (false in preview/draft mode). */
20
+ enabled: boolean;
21
+ /** The current A/B test variant context, if any. */
22
+ abnVariant: ABnVariantContext | null;
23
+ /** Set the A/B variant context. Called by ABnVariantSetter from page components. */
24
+ setABnVariant(variant: ABnVariantContext | null): void;
25
+ /** When true, ALL forms automatically track form_start, form_submit, and form_error regardless of per-form settings. */
26
+ globalFormTracking: boolean;
27
+ }
28
+
29
+ /**
30
+ * CMS Data Layer
31
+ *
32
+ * Central event bus for analytics events. All events flow through here.
33
+ * Provider adapters subscribe and translate events to provider-specific calls.
34
+ */
35
+
36
+ declare global {
37
+ interface Window {
38
+ __cms?: CMSDataLayer;
39
+ }
40
+ }
41
+ declare class CMSDataLayer {
42
+ readonly entries: DataLayerEntry[];
43
+ private readonly subscribers;
44
+ push(entry: DataLayerEntry): void;
45
+ subscribe(fn: DataLayerSubscriber): () => void;
46
+ }
47
+ declare const dataLayer: CMSDataLayer;
48
+
49
+ declare function useAnalytics(): AnalyticsContextValue;
50
+ interface AnalyticsProviderProps {
51
+ enabled?: boolean;
52
+ /** When true, all forms automatically emit form_start, form_submit, form_error. */
53
+ globalFormTracking?: boolean;
54
+ children: React.ReactNode;
55
+ }
56
+ declare function AnalyticsProvider({ enabled, globalFormTracking, children, }: AnalyticsProviderProps): react_jsx_runtime.JSX.Element;
57
+
58
+ interface BlockAnalyticsWrapperProps {
59
+ analyticsConfig: BlockAnalyticsConfig | undefined;
60
+ blockId: string;
61
+ blockType: string;
62
+ children: React.ReactNode;
63
+ }
64
+ declare function BlockAnalyticsWrapper({ analyticsConfig, blockId, blockType, children, }: BlockAnalyticsWrapperProps): react_jsx_runtime.JSX.Element;
65
+
66
+ interface SectionAnalyticsWrapperProps {
67
+ analyticsConfig: BlockAnalyticsConfig | undefined;
68
+ sectionId: string;
69
+ sectionType: string;
70
+ children: React.ReactNode;
71
+ }
72
+ declare function SectionAnalyticsWrapper({ analyticsConfig, sectionId, sectionType, children, }: SectionAnalyticsWrapperProps): react_jsx_runtime.JSX.Element;
73
+
74
+ interface ABnVariantSetterProps {
75
+ variantId: string;
76
+ experimentId?: string;
77
+ }
78
+ /**
79
+ * A client component that page components render to communicate the
80
+ * resolved A/B test variant to the analytics context.
81
+ *
82
+ * Renders nothing -- just sets context on mount and emits experiment_view.
83
+ */
84
+ declare function ABnVariantSetter({ variantId, experimentId, }: ABnVariantSetterProps): null;
85
+
86
+ export { type ABnVariantContext, ABnVariantSetter, type AnalyticsContextValue, AnalyticsProvider, BlockAnalyticsWrapper, CMSDataLayer, type DataLayerEntry, type DataLayerSubscriber, SectionAnalyticsWrapper, dataLayer, useAnalytics };
package/dist/index.js ADDED
@@ -0,0 +1,278 @@
1
+ "use client";
2
+ import { createContext, useContext, useState, useCallback, useMemo, useRef, useEffect } from 'react';
3
+ import { jsx, Fragment } from 'react/jsx-runtime';
4
+
5
+ // src/data-layer.ts
6
+ var CMSDataLayer = class {
7
+ constructor() {
8
+ this.entries = [];
9
+ this.subscribers = /* @__PURE__ */ new Set();
10
+ }
11
+ push(entry) {
12
+ this.entries.push(entry);
13
+ for (const fn of this.subscribers) {
14
+ try {
15
+ fn(entry);
16
+ } catch {
17
+ }
18
+ }
19
+ }
20
+ subscribe(fn) {
21
+ for (const entry of this.entries) {
22
+ try {
23
+ fn(entry);
24
+ } catch {
25
+ }
26
+ }
27
+ this.subscribers.add(fn);
28
+ return () => {
29
+ this.subscribers.delete(fn);
30
+ };
31
+ }
32
+ };
33
+ var dataLayer = new CMSDataLayer();
34
+ if (typeof window !== "undefined") {
35
+ window.__cms = dataLayer;
36
+ }
37
+ var AnalyticsContext = createContext({
38
+ trackEvent: () => {
39
+ },
40
+ enabled: false,
41
+ abnVariant: null,
42
+ setABnVariant: () => {
43
+ },
44
+ globalFormTracking: false
45
+ });
46
+ function useAnalytics() {
47
+ return useContext(AnalyticsContext);
48
+ }
49
+ function AnalyticsProvider({
50
+ enabled = true,
51
+ globalFormTracking = false,
52
+ children
53
+ }) {
54
+ const [abnVariant, setABnVariant] = useState(null);
55
+ const trackEvent = useCallback(
56
+ (event, params) => {
57
+ if (!enabled) return;
58
+ const variantParams = {};
59
+ if (abnVariant) {
60
+ variantParams.abn_variant_id = abnVariant.variantId;
61
+ if (abnVariant.experimentId) {
62
+ variantParams.abn_experiment_id = abnVariant.experimentId;
63
+ }
64
+ }
65
+ dataLayer.push({
66
+ event,
67
+ timestamp: Date.now(),
68
+ ...variantParams,
69
+ ...params
70
+ });
71
+ },
72
+ [enabled, abnVariant]
73
+ );
74
+ const value = useMemo(
75
+ () => ({
76
+ trackEvent,
77
+ enabled,
78
+ abnVariant,
79
+ setABnVariant,
80
+ globalFormTracking
81
+ }),
82
+ [trackEvent, enabled, abnVariant, globalFormTracking]
83
+ );
84
+ return /* @__PURE__ */ jsx(AnalyticsContext.Provider, { value, children });
85
+ }
86
+ function BlockAnalyticsWrapper({
87
+ analyticsConfig,
88
+ blockId,
89
+ blockType,
90
+ children
91
+ }) {
92
+ const wrapperRef = useRef(null);
93
+ if (!analyticsConfig || !analyticsConfig.enabled) {
94
+ return /* @__PURE__ */ jsx(Fragment, { children });
95
+ }
96
+ return /* @__PURE__ */ jsx(
97
+ BlockAnalyticsInner,
98
+ {
99
+ analyticsConfig,
100
+ blockId,
101
+ blockType,
102
+ wrapperRef,
103
+ children
104
+ }
105
+ );
106
+ }
107
+ function BlockAnalyticsInner({
108
+ analyticsConfig,
109
+ blockId,
110
+ blockType,
111
+ wrapperRef,
112
+ children
113
+ }) {
114
+ const {
115
+ event_label,
116
+ track_type,
117
+ visibility_threshold = 50,
118
+ fire_once = true,
119
+ target_providers = "all",
120
+ custom_params = {}
121
+ } = analyticsConfig;
122
+ const baseParams = {
123
+ block_id: blockId,
124
+ block_type: blockType,
125
+ event_label,
126
+ ...custom_params,
127
+ ...target_providers !== "all" ? { _target_providers: target_providers } : {}
128
+ };
129
+ const handleClick = useCallback(() => {
130
+ if (track_type === "click" || track_type === "both") {
131
+ dataLayer.push({
132
+ event: "block_click",
133
+ timestamp: Date.now(),
134
+ ...baseParams
135
+ });
136
+ }
137
+ }, [track_type, blockId, blockType, event_label]);
138
+ useEffect(() => {
139
+ if (track_type !== "visibility" && track_type !== "both") return;
140
+ if (!wrapperRef.current) return;
141
+ const threshold = Math.min(Math.max(visibility_threshold, 0), 100) / 100;
142
+ const observer = new IntersectionObserver(
143
+ (entries) => {
144
+ for (const entry of entries) {
145
+ if (entry.isIntersecting) {
146
+ dataLayer.push({
147
+ event: "block_visible",
148
+ timestamp: Date.now(),
149
+ ...baseParams
150
+ });
151
+ if (fire_once) {
152
+ observer.disconnect();
153
+ }
154
+ }
155
+ }
156
+ },
157
+ { threshold }
158
+ );
159
+ observer.observe(wrapperRef.current);
160
+ return () => {
161
+ observer.disconnect();
162
+ };
163
+ }, [
164
+ track_type,
165
+ visibility_threshold,
166
+ fire_once,
167
+ blockId,
168
+ blockType,
169
+ event_label
170
+ ]);
171
+ return /* @__PURE__ */ jsx("div", { ref: wrapperRef, onClick: handleClick, children });
172
+ }
173
+ function SectionAnalyticsWrapper({
174
+ analyticsConfig,
175
+ sectionId,
176
+ sectionType,
177
+ children
178
+ }) {
179
+ const wrapperRef = useRef(null);
180
+ if (!analyticsConfig || !analyticsConfig.enabled) {
181
+ return /* @__PURE__ */ jsx(Fragment, { children });
182
+ }
183
+ return /* @__PURE__ */ jsx(
184
+ SectionAnalyticsInner,
185
+ {
186
+ analyticsConfig,
187
+ sectionId,
188
+ sectionType,
189
+ wrapperRef,
190
+ children
191
+ }
192
+ );
193
+ }
194
+ function SectionAnalyticsInner({
195
+ analyticsConfig,
196
+ sectionId,
197
+ sectionType,
198
+ wrapperRef,
199
+ children
200
+ }) {
201
+ const {
202
+ event_label,
203
+ track_type,
204
+ visibility_threshold = 50,
205
+ fire_once = true,
206
+ target_providers = "all",
207
+ custom_params = {}
208
+ } = analyticsConfig;
209
+ const baseParams = {
210
+ section_id: sectionId,
211
+ section_type: sectionType,
212
+ event_label,
213
+ ...custom_params,
214
+ ...target_providers !== "all" ? { _target_providers: target_providers } : {}
215
+ };
216
+ const handleClick = useCallback(() => {
217
+ if (track_type === "click" || track_type === "both") {
218
+ dataLayer.push({
219
+ event: "block_click",
220
+ timestamp: Date.now(),
221
+ ...baseParams
222
+ });
223
+ }
224
+ }, [track_type, sectionId, sectionType, event_label]);
225
+ useEffect(() => {
226
+ if (track_type !== "visibility" && track_type !== "both") return;
227
+ if (!wrapperRef.current) return;
228
+ const threshold = Math.min(Math.max(visibility_threshold, 0), 100) / 100;
229
+ const observer = new IntersectionObserver(
230
+ (entries) => {
231
+ for (const entry of entries) {
232
+ if (entry.isIntersecting) {
233
+ dataLayer.push({
234
+ event: "block_visible",
235
+ timestamp: Date.now(),
236
+ ...baseParams
237
+ });
238
+ if (fire_once) {
239
+ observer.disconnect();
240
+ }
241
+ }
242
+ }
243
+ },
244
+ { threshold }
245
+ );
246
+ observer.observe(wrapperRef.current);
247
+ return () => {
248
+ observer.disconnect();
249
+ };
250
+ }, [
251
+ track_type,
252
+ visibility_threshold,
253
+ fire_once,
254
+ sectionId,
255
+ sectionType,
256
+ event_label
257
+ ]);
258
+ return /* @__PURE__ */ jsx("div", { ref: wrapperRef, onClick: handleClick, children });
259
+ }
260
+ function ABnVariantSetter({
261
+ variantId,
262
+ experimentId
263
+ }) {
264
+ const { setABnVariant, trackEvent } = useAnalytics();
265
+ useEffect(() => {
266
+ setABnVariant({ variantId, experimentId });
267
+ trackEvent("experiment_view", {
268
+ abn_variant_id: variantId,
269
+ abn_experiment_id: experimentId
270
+ });
271
+ return () => setABnVariant(null);
272
+ }, [variantId, experimentId, setABnVariant, trackEvent]);
273
+ return null;
274
+ }
275
+
276
+ export { ABnVariantSetter, AnalyticsProvider, BlockAnalyticsWrapper, CMSDataLayer, SectionAnalyticsWrapper, dataLayer, useAnalytics };
277
+ //# sourceMappingURL=index.js.map
278
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/data-layer.ts","../src/analytics-context.tsx","../src/block-analytics-wrapper.tsx","../src/section-analytics-wrapper.tsx","../src/abn-variant-setter.tsx"],"names":["jsx","useCallback","useRef","Fragment","useEffect"],"mappings":";;;;AAeO,IAAM,eAAN,MAAmB;AAAA,EAAnB,WAAA,GAAA;AACL,IAAA,IAAA,CAAS,UAA4B,EAAC;AACtC,IAAA,IAAA,CAAiB,WAAA,uBAAkB,GAAA,EAAyB;AAAA,EAAA;AAAA,EAE5D,KAAK,KAAA,EAA6B;AAChC,IAAA,IAAA,CAAK,OAAA,CAAQ,KAAK,KAAK,CAAA;AACvB,IAAA,KAAA,MAAW,EAAA,IAAM,KAAK,WAAA,EAAa;AACjC,MAAA,IAAI;AACF,QAAA,EAAA,CAAG,KAAK,CAAA;AAAA,MACV,CAAA,CAAA,MAAQ;AAAA,MAER;AAAA,IACF;AAAA,EACF;AAAA,EAEA,UAAU,EAAA,EAAqC;AAC7C,IAAA,KAAA,MAAW,KAAA,IAAS,KAAK,OAAA,EAAS;AAChC,MAAA,IAAI;AACF,QAAA,EAAA,CAAG,KAAK,CAAA;AAAA,MACV,CAAA,CAAA,MAAQ;AAAA,MAER;AAAA,IACF;AACA,IAAA,IAAA,CAAK,WAAA,CAAY,IAAI,EAAE,CAAA;AACvB,IAAA,OAAO,MAAM;AACX,MAAA,IAAA,CAAK,WAAA,CAAY,OAAO,EAAE,CAAA;AAAA,IAC5B,CAAA;AAAA,EACF;AACF;AAEO,IAAM,SAAA,GAAY,IAAI,YAAA;AAE7B,IAAI,OAAO,WAAW,WAAA,EAAa;AACjC,EAAA,MAAA,CAAO,KAAA,GAAQ,SAAA;AACjB;ACpCA,IAAM,mBAAmB,aAAA,CAAqC;AAAA,EAC5D,YAAY,MAAM;AAAA,EAAC,CAAA;AAAA,EACnB,OAAA,EAAS,KAAA;AAAA,EACT,UAAA,EAAY,IAAA;AAAA,EACZ,eAAe,MAAM;AAAA,EAAC,CAAA;AAAA,EACtB,kBAAA,EAAoB;AACtB,CAAC,CAAA;AAEM,SAAS,YAAA,GAAsC;AACpD,EAAA,OAAO,WAAW,gBAAgB,CAAA;AACpC;AASO,SAAS,iBAAA,CAAkB;AAAA,EAChC,OAAA,GAAU,IAAA;AAAA,EACV,kBAAA,GAAqB,KAAA;AAAA,EACrB;AACF,CAAA,EAA2B;AACzB,EAAA,MAAM,CAAC,UAAA,EAAY,aAAa,CAAA,GAAI,SAAmC,IAAI,CAAA;AAE3E,EAAA,MAAM,UAAA,GAAa,WAAA;AAAA,IACjB,CAAC,OAA0B,MAAA,KAAqC;AAC9D,MAAA,IAAI,CAAC,OAAA,EAAS;AACd,MAAA,MAAM,gBAAyC,EAAC;AAChD,MAAA,IAAI,UAAA,EAAY;AACd,QAAA,aAAA,CAAc,iBAAiB,UAAA,CAAW,SAAA;AAC1C,QAAA,IAAI,WAAW,YAAA,EAAc;AAC3B,UAAA,aAAA,CAAc,oBAAoB,UAAA,CAAW,YAAA;AAAA,QAC/C;AAAA,MACF;AACA,MAAA,SAAA,CAAU,IAAA,CAAK;AAAA,QACb,KAAA;AAAA,QACA,SAAA,EAAW,KAAK,GAAA,EAAI;AAAA,QACpB,GAAG,aAAA;AAAA,QACH,GAAG;AAAA,OACJ,CAAA;AAAA,IACH,CAAA;AAAA,IACA,CAAC,SAAS,UAAU;AAAA,GACtB;AAEA,EAAA,MAAM,KAAA,GAAQ,OAAA;AAAA,IACZ,OAAO;AAAA,MACL,UAAA;AAAA,MACA,OAAA;AAAA,MACA,UAAA;AAAA,MACA,aAAA;AAAA,MACA;AAAA,KACF,CAAA;AAAA,IACA,CAAC,UAAA,EAAY,OAAA,EAAS,UAAA,EAAY,kBAAkB;AAAA,GACtD;AAEA,EAAA,uBACE,GAAA,CAAC,gBAAA,CAAiB,QAAA,EAAjB,EAA0B,OACxB,QAAA,EACH,CAAA;AAEJ;ACxDO,SAAS,qBAAA,CAAsB;AAAA,EACpC,eAAA;AAAA,EACA,OAAA;AAAA,EACA,SAAA;AAAA,EACA;AACF,CAAA,EAA+B;AAC7B,EAAA,MAAM,UAAA,GAAa,OAAuB,IAAI,CAAA;AAE9C,EAAA,IAAI,CAAC,eAAA,IAAmB,CAAC,eAAA,CAAgB,OAAA,EAAS;AAChD,IAAA,uBAAOA,GAAAA,CAAA,QAAA,EAAA,EAAG,QAAA,EAAS,CAAA;AAAA,EACrB;AAEA,EAAA,uBACEA,GAAAA;AAAA,IAAC,mBAAA;AAAA,IAAA;AAAA,MACC,eAAA;AAAA,MACA,OAAA;AAAA,MACA,SAAA;AAAA,MACA,UAAA;AAAA,MAEC;AAAA;AAAA,GACH;AAEJ;AAUA,SAAS,mBAAA,CAAoB;AAAA,EAC3B,eAAA;AAAA,EACA,OAAA;AAAA,EACA,SAAA;AAAA,EACA,UAAA;AAAA,EACA;AACF,CAAA,EAA6B;AAC3B,EAAA,MAAM;AAAA,IACJ,WAAA;AAAA,IACA,UAAA;AAAA,IACA,oBAAA,GAAuB,EAAA;AAAA,IACvB,SAAA,GAAY,IAAA;AAAA,IACZ,gBAAA,GAAmB,KAAA;AAAA,IACnB,gBAAgB;AAAC,GACnB,GAAI,eAAA;AAEJ,EAAA,MAAM,UAAA,GAAa;AAAA,IACjB,QAAA,EAAU,OAAA;AAAA,IACV,UAAA,EAAY,SAAA;AAAA,IACZ,WAAA;AAAA,IACA,GAAG,aAAA;AAAA,IACH,GAAI,gBAAA,KAAqB,KAAA,GACrB,EAAE,iBAAA,EAAmB,gBAAA,KACrB;AAAC,GACP;AAEA,EAAA,MAAM,WAAA,GAAcC,YAAY,MAAM;AACpC,IAAA,IAAI,UAAA,KAAe,OAAA,IAAW,UAAA,KAAe,MAAA,EAAQ;AACnD,MAAA,SAAA,CAAU,IAAA,CAAK;AAAA,QACb,KAAA,EAAO,aAAA;AAAA,QACP,SAAA,EAAW,KAAK,GAAA,EAAI;AAAA,QACpB,GAAG;AAAA,OACJ,CAAA;AAAA,IACH;AAAA,EAEF,GAAG,CAAC,UAAA,EAAY,OAAA,EAAS,SAAA,EAAW,WAAW,CAAC,CAAA;AAEhD,EAAA,SAAA,CAAU,MAAM;AACd,IAAA,IAAI,UAAA,KAAe,YAAA,IAAgB,UAAA,KAAe,MAAA,EAAQ;AAC1D,IAAA,IAAI,CAAC,WAAW,OAAA,EAAS;AAEzB,IAAA,MAAM,SAAA,GAAY,KAAK,GAAA,CAAI,IAAA,CAAK,IAAI,oBAAA,EAAsB,CAAC,CAAA,EAAG,GAAG,CAAA,GAAI,GAAA;AACrE,IAAA,MAAM,WAAW,IAAI,oBAAA;AAAA,MACnB,CAAA,OAAA,KAAW;AACT,QAAA,KAAA,MAAW,SAAS,OAAA,EAAS;AAC3B,UAAA,IAAI,MAAM,cAAA,EAAgB;AACxB,YAAA,SAAA,CAAU,IAAA,CAAK;AAAA,cACb,KAAA,EAAO,eAAA;AAAA,cACP,SAAA,EAAW,KAAK,GAAA,EAAI;AAAA,cACpB,GAAG;AAAA,aACJ,CAAA;AAED,YAAA,IAAI,SAAA,EAAW;AACb,cAAA,QAAA,CAAS,UAAA,EAAW;AAAA,YACtB;AAAA,UACF;AAAA,QACF;AAAA,MACF,CAAA;AAAA,MACA,EAAE,SAAA;AAAU,KACd;AAEA,IAAA,QAAA,CAAS,OAAA,CAAQ,WAAW,OAAO,CAAA;AAEnC,IAAA,OAAO,MAAM;AACX,MAAA,QAAA,CAAS,UAAA,EAAW;AAAA,IACtB,CAAA;AAAA,EAEF,CAAA,EAAG;AAAA,IACD,UAAA;AAAA,IACA,oBAAA;AAAA,IACA,SAAA;AAAA,IACA,OAAA;AAAA,IACA,SAAA;AAAA,IACA;AAAA,GACD,CAAA;AAED,EAAA,uBACED,GAAAA,CAAC,KAAA,EAAA,EAAI,KAAK,UAAA,EAAY,OAAA,EAAS,aAC5B,QAAA,EACH,CAAA;AAEJ;ACjHO,SAAS,uBAAA,CAAwB;AAAA,EACtC,eAAA;AAAA,EACA,SAAA;AAAA,EACA,WAAA;AAAA,EACA;AACF,CAAA,EAAiC;AAC/B,EAAA,MAAM,UAAA,GAAaE,OAAuB,IAAI,CAAA;AAE9C,EAAA,IAAI,CAAC,eAAA,IAAmB,CAAC,eAAA,CAAgB,OAAA,EAAS;AAChD,IAAA,uBAAOF,GAAAA,CAAAG,QAAAA,EAAA,EAAG,QAAA,EAAS,CAAA;AAAA,EACrB;AAEA,EAAA,uBACEH,GAAAA;AAAA,IAAC,qBAAA;AAAA,IAAA;AAAA,MACC,eAAA;AAAA,MACA,SAAA;AAAA,MACA,WAAA;AAAA,MACA,UAAA;AAAA,MAEC;AAAA;AAAA,GACH;AAEJ;AAUA,SAAS,qBAAA,CAAsB;AAAA,EAC7B,eAAA;AAAA,EACA,SAAA;AAAA,EACA,WAAA;AAAA,EACA,UAAA;AAAA,EACA;AACF,CAAA,EAA+B;AAC7B,EAAA,MAAM;AAAA,IACJ,WAAA;AAAA,IACA,UAAA;AAAA,IACA,oBAAA,GAAuB,EAAA;AAAA,IACvB,SAAA,GAAY,IAAA;AAAA,IACZ,gBAAA,GAAmB,KAAA;AAAA,IACnB,gBAAgB;AAAC,GACnB,GAAI,eAAA;AAEJ,EAAA,MAAM,UAAA,GAAa;AAAA,IACjB,UAAA,EAAY,SAAA;AAAA,IACZ,YAAA,EAAc,WAAA;AAAA,IACd,WAAA;AAAA,IACA,GAAG,aAAA;AAAA,IACH,GAAI,gBAAA,KAAqB,KAAA,GACrB,EAAE,iBAAA,EAAmB,gBAAA,KACrB;AAAC,GACP;AAEA,EAAA,MAAM,WAAA,GAAcC,YAAY,MAAM;AACpC,IAAA,IAAI,UAAA,KAAe,OAAA,IAAW,UAAA,KAAe,MAAA,EAAQ;AACnD,MAAA,SAAA,CAAU,IAAA,CAAK;AAAA,QACb,KAAA,EAAO,aAAA;AAAA,QACP,SAAA,EAAW,KAAK,GAAA,EAAI;AAAA,QACpB,GAAG;AAAA,OACJ,CAAA;AAAA,IACH;AAAA,EAEF,GAAG,CAAC,UAAA,EAAY,SAAA,EAAW,WAAA,EAAa,WAAW,CAAC,CAAA;AAEpD,EAAAG,UAAU,MAAM;AACd,IAAA,IAAI,UAAA,KAAe,YAAA,IAAgB,UAAA,KAAe,MAAA,EAAQ;AAC1D,IAAA,IAAI,CAAC,WAAW,OAAA,EAAS;AAEzB,IAAA,MAAM,SAAA,GAAY,KAAK,GAAA,CAAI,IAAA,CAAK,IAAI,oBAAA,EAAsB,CAAC,CAAA,EAAG,GAAG,CAAA,GAAI,GAAA;AACrE,IAAA,MAAM,WAAW,IAAI,oBAAA;AAAA,MACnB,CAAA,OAAA,KAAW;AACT,QAAA,KAAA,MAAW,SAAS,OAAA,EAAS;AAC3B,UAAA,IAAI,MAAM,cAAA,EAAgB;AACxB,YAAA,SAAA,CAAU,IAAA,CAAK;AAAA,cACb,KAAA,EAAO,eAAA;AAAA,cACP,SAAA,EAAW,KAAK,GAAA,EAAI;AAAA,cACpB,GAAG;AAAA,aACJ,CAAA;AAED,YAAA,IAAI,SAAA,EAAW;AACb,cAAA,QAAA,CAAS,UAAA,EAAW;AAAA,YACtB;AAAA,UACF;AAAA,QACF;AAAA,MACF,CAAA;AAAA,MACA,EAAE,SAAA;AAAU,KACd;AAEA,IAAA,QAAA,CAAS,OAAA,CAAQ,WAAW,OAAO,CAAA;AAEnC,IAAA,OAAO,MAAM;AACX,MAAA,QAAA,CAAS,UAAA,EAAW;AAAA,IACtB,CAAA;AAAA,EAEF,CAAA,EAAG;AAAA,IACD,UAAA;AAAA,IACA,oBAAA;AAAA,IACA,SAAA;AAAA,IACA,SAAA;AAAA,IACA,WAAA;AAAA,IACA;AAAA,GACD,CAAA;AAED,EAAA,uBACEJ,GAAAA,CAAC,KAAA,EAAA,EAAI,KAAK,UAAA,EAAY,OAAA,EAAS,aAC5B,QAAA,EACH,CAAA;AAEJ;ACnHO,SAAS,gBAAA,CAAiB;AAAA,EAC/B,SAAA;AAAA,EACA;AACF,CAAA,EAA0B;AACxB,EAAA,MAAM,EAAE,aAAA,EAAe,UAAA,EAAW,GAAI,YAAA,EAAa;AAEnD,EAAAI,UAAU,MAAM;AACd,IAAA,aAAA,CAAc,EAAE,SAAA,EAAW,YAAA,EAAc,CAAA;AACzC,IAAA,UAAA,CAAW,iBAAA,EAAwC;AAAA,MACjD,cAAA,EAAgB,SAAA;AAAA,MAChB,iBAAA,EAAmB;AAAA,KACpB,CAAA;AACD,IAAA,OAAO,MAAM,cAAc,IAAI,CAAA;AAAA,EACjC,GAAG,CAAC,SAAA,EAAW,YAAA,EAAc,aAAA,EAAe,UAAU,CAAC,CAAA;AAEvD,EAAA,OAAO,IAAA;AACT","file":"index.js","sourcesContent":["/**\n * CMS Data Layer\n *\n * Central event bus for analytics events. All events flow through here.\n * Provider adapters subscribe and translate events to provider-specific calls.\n */\n\nimport type { DataLayerEntry, DataLayerSubscriber } from \"./types\";\n\ndeclare global {\n interface Window {\n __cms?: CMSDataLayer;\n }\n}\n\nexport class CMSDataLayer {\n readonly entries: DataLayerEntry[] = [];\n private readonly subscribers = new Set<DataLayerSubscriber>();\n\n push(entry: DataLayerEntry): void {\n this.entries.push(entry);\n for (const fn of this.subscribers) {\n try {\n fn(entry);\n } catch {\n // Swallow subscriber errors to prevent cascade failures\n }\n }\n }\n\n subscribe(fn: DataLayerSubscriber): () => void {\n for (const entry of this.entries) {\n try {\n fn(entry);\n } catch {\n // Swallow replay errors\n }\n }\n this.subscribers.add(fn);\n return () => {\n this.subscribers.delete(fn);\n };\n }\n}\n\nexport const dataLayer = new CMSDataLayer();\n\nif (typeof window !== \"undefined\") {\n window.__cms = dataLayer;\n}\n","\"use client\";\n\nimport {\n createContext,\n useCallback,\n useContext,\n useMemo,\n useState,\n} from \"react\";\nimport type { StandardEventName } from \"@otl-core/cms-types\";\nimport type { ABnVariantContext, AnalyticsContextValue } from \"./types\";\nimport { dataLayer } from \"./data-layer\";\n\nconst AnalyticsContext = createContext<AnalyticsContextValue>({\n trackEvent: () => {},\n enabled: false,\n abnVariant: null,\n setABnVariant: () => {},\n globalFormTracking: false,\n});\n\nexport function useAnalytics(): AnalyticsContextValue {\n return useContext(AnalyticsContext);\n}\n\ninterface AnalyticsProviderProps {\n enabled?: boolean;\n /** When true, all forms automatically emit form_start, form_submit, form_error. */\n globalFormTracking?: boolean;\n children: React.ReactNode;\n}\n\nexport function AnalyticsProvider({\n enabled = true,\n globalFormTracking = false,\n children,\n}: AnalyticsProviderProps) {\n const [abnVariant, setABnVariant] = useState<ABnVariantContext | null>(null);\n\n const trackEvent = useCallback(\n (event: StandardEventName, params?: Record<string, unknown>) => {\n if (!enabled) return;\n const variantParams: Record<string, unknown> = {};\n if (abnVariant) {\n variantParams.abn_variant_id = abnVariant.variantId;\n if (abnVariant.experimentId) {\n variantParams.abn_experiment_id = abnVariant.experimentId;\n }\n }\n dataLayer.push({\n event,\n timestamp: Date.now(),\n ...variantParams,\n ...params,\n });\n },\n [enabled, abnVariant]\n );\n\n const value = useMemo<AnalyticsContextValue>(\n () => ({\n trackEvent,\n enabled,\n abnVariant,\n setABnVariant,\n globalFormTracking,\n }),\n [trackEvent, enabled, abnVariant, globalFormTracking]\n );\n\n return (\n <AnalyticsContext.Provider value={value}>\n {children}\n </AnalyticsContext.Provider>\n );\n}\n","\"use client\";\n\n/**\n * Block Analytics Wrapper\n *\n * Wraps a block and adds click/visibility tracking based on BlockAnalyticsConfig.\n */\n\nimport { useCallback, useEffect, useRef } from \"react\";\nimport type { BlockAnalyticsConfig } from \"@otl-core/cms-types\";\nimport { dataLayer } from \"./data-layer\";\n\ninterface BlockAnalyticsWrapperProps {\n analyticsConfig: BlockAnalyticsConfig | undefined;\n blockId: string;\n blockType: string;\n children: React.ReactNode;\n}\n\nexport function BlockAnalyticsWrapper({\n analyticsConfig,\n blockId,\n blockType,\n children,\n}: BlockAnalyticsWrapperProps) {\n const wrapperRef = useRef<HTMLDivElement>(null);\n\n if (!analyticsConfig || !analyticsConfig.enabled) {\n return <>{children}</>;\n }\n\n return (\n <BlockAnalyticsInner\n analyticsConfig={analyticsConfig}\n blockId={blockId}\n blockType={blockType}\n wrapperRef={wrapperRef}\n >\n {children}\n </BlockAnalyticsInner>\n );\n}\n\ninterface BlockAnalyticsInnerProps {\n analyticsConfig: BlockAnalyticsConfig;\n blockId: string;\n blockType: string;\n wrapperRef: React.RefObject<HTMLDivElement | null>;\n children: React.ReactNode;\n}\n\nfunction BlockAnalyticsInner({\n analyticsConfig,\n blockId,\n blockType,\n wrapperRef,\n children,\n}: BlockAnalyticsInnerProps) {\n const {\n event_label,\n track_type,\n visibility_threshold = 50,\n fire_once = true,\n target_providers = \"all\",\n custom_params = {},\n } = analyticsConfig;\n\n const baseParams = {\n block_id: blockId,\n block_type: blockType,\n event_label,\n ...custom_params,\n ...(target_providers !== \"all\"\n ? { _target_providers: target_providers }\n : {}),\n };\n\n const handleClick = useCallback(() => {\n if (track_type === \"click\" || track_type === \"both\") {\n dataLayer.push({\n event: \"block_click\",\n timestamp: Date.now(),\n ...baseParams,\n });\n }\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [track_type, blockId, blockType, event_label]);\n\n useEffect(() => {\n if (track_type !== \"visibility\" && track_type !== \"both\") return;\n if (!wrapperRef.current) return;\n\n const threshold = Math.min(Math.max(visibility_threshold, 0), 100) / 100;\n const observer = new IntersectionObserver(\n entries => {\n for (const entry of entries) {\n if (entry.isIntersecting) {\n dataLayer.push({\n event: \"block_visible\",\n timestamp: Date.now(),\n ...baseParams,\n });\n\n if (fire_once) {\n observer.disconnect();\n }\n }\n }\n },\n { threshold }\n );\n\n observer.observe(wrapperRef.current);\n\n return () => {\n observer.disconnect();\n };\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [\n track_type,\n visibility_threshold,\n fire_once,\n blockId,\n blockType,\n event_label,\n ]);\n\n return (\n <div ref={wrapperRef} onClick={handleClick}>\n {children}\n </div>\n );\n}\n","\"use client\";\n\n/**\n * Section Analytics Wrapper\n *\n * Wraps a section and adds click/visibility tracking based on BlockAnalyticsConfig.\n */\n\nimport { useCallback, useEffect, useRef } from \"react\";\nimport type { BlockAnalyticsConfig } from \"@otl-core/cms-types\";\nimport { dataLayer } from \"./data-layer\";\n\ninterface SectionAnalyticsWrapperProps {\n analyticsConfig: BlockAnalyticsConfig | undefined;\n sectionId: string;\n sectionType: string;\n children: React.ReactNode;\n}\n\nexport function SectionAnalyticsWrapper({\n analyticsConfig,\n sectionId,\n sectionType,\n children,\n}: SectionAnalyticsWrapperProps) {\n const wrapperRef = useRef<HTMLDivElement>(null);\n\n if (!analyticsConfig || !analyticsConfig.enabled) {\n return <>{children}</>;\n }\n\n return (\n <SectionAnalyticsInner\n analyticsConfig={analyticsConfig}\n sectionId={sectionId}\n sectionType={sectionType}\n wrapperRef={wrapperRef}\n >\n {children}\n </SectionAnalyticsInner>\n );\n}\n\ninterface SectionAnalyticsInnerProps {\n analyticsConfig: BlockAnalyticsConfig;\n sectionId: string;\n sectionType: string;\n wrapperRef: React.RefObject<HTMLDivElement | null>;\n children: React.ReactNode;\n}\n\nfunction SectionAnalyticsInner({\n analyticsConfig,\n sectionId,\n sectionType,\n wrapperRef,\n children,\n}: SectionAnalyticsInnerProps) {\n const {\n event_label,\n track_type,\n visibility_threshold = 50,\n fire_once = true,\n target_providers = \"all\",\n custom_params = {},\n } = analyticsConfig;\n\n const baseParams = {\n section_id: sectionId,\n section_type: sectionType,\n event_label,\n ...custom_params,\n ...(target_providers !== \"all\"\n ? { _target_providers: target_providers }\n : {}),\n };\n\n const handleClick = useCallback(() => {\n if (track_type === \"click\" || track_type === \"both\") {\n dataLayer.push({\n event: \"block_click\",\n timestamp: Date.now(),\n ...baseParams,\n });\n }\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [track_type, sectionId, sectionType, event_label]);\n\n useEffect(() => {\n if (track_type !== \"visibility\" && track_type !== \"both\") return;\n if (!wrapperRef.current) return;\n\n const threshold = Math.min(Math.max(visibility_threshold, 0), 100) / 100;\n const observer = new IntersectionObserver(\n entries => {\n for (const entry of entries) {\n if (entry.isIntersecting) {\n dataLayer.push({\n event: \"block_visible\",\n timestamp: Date.now(),\n ...baseParams,\n });\n\n if (fire_once) {\n observer.disconnect();\n }\n }\n }\n },\n { threshold }\n );\n\n observer.observe(wrapperRef.current);\n\n return () => {\n observer.disconnect();\n };\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [\n track_type,\n visibility_threshold,\n fire_once,\n sectionId,\n sectionType,\n event_label,\n ]);\n\n return (\n <div ref={wrapperRef} onClick={handleClick}>\n {children}\n </div>\n );\n}\n","\"use client\";\n\nimport { useEffect } from \"react\";\nimport type { StandardEventName } from \"@otl-core/cms-types\";\nimport { useAnalytics } from \"./analytics-context\";\n\ninterface ABnVariantSetterProps {\n variantId: string;\n experimentId?: string;\n}\n\n/**\n * A client component that page components render to communicate the\n * resolved A/B test variant to the analytics context.\n *\n * Renders nothing -- just sets context on mount and emits experiment_view.\n */\nexport function ABnVariantSetter({\n variantId,\n experimentId,\n}: ABnVariantSetterProps) {\n const { setABnVariant, trackEvent } = useAnalytics();\n\n useEffect(() => {\n setABnVariant({ variantId, experimentId });\n trackEvent(\"experiment_view\" as StandardEventName, {\n abn_variant_id: variantId,\n abn_experiment_id: experimentId,\n });\n return () => setABnVariant(null);\n }, [variantId, experimentId, setABnVariant, trackEvent]);\n\n return null;\n}\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@otl-core/analytics",
3
- "version": "1.0.0",
3
+ "version": "1.0.4",
4
4
  "type": "module",
5
5
  "description": "Analytics event bus and React context for OTL CMS",
6
6
  "main": "./dist/index.cjs",
@@ -33,7 +33,7 @@
33
33
  "url": "https://github.com/otl-core/analytics.git"
34
34
  },
35
35
  "dependencies": {
36
- "@otl-core/cms-types": "^1.0.0"
36
+ "@otl-core/cms-types": "^1.0.4"
37
37
  },
38
38
  "peerDependencies": {
39
39
  "react": "^18.0.0 || ^19.0.0"