@jwiedeman/gtm-kit 1.0.1

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.
@@ -0,0 +1,431 @@
1
+ declare const CONSENT_COMMAND: "consent";
2
+ declare const CONSENT_DEFAULT: "default";
3
+ declare const CONSENT_UPDATE: "update";
4
+ /**
5
+ * The four consent categories tracked by Google Consent Mode v2.
6
+ */
7
+ declare const CONSENT_KEYS: readonly ["ad_storage", "analytics_storage", "ad_user_data", "ad_personalization"];
8
+ /**
9
+ * A consent category key: 'ad_storage' | 'analytics_storage' | 'ad_user_data' | 'ad_personalization'
10
+ */
11
+ type ConsentKey = (typeof CONSENT_KEYS)[number];
12
+ /**
13
+ * A consent decision: 'granted' or 'denied'
14
+ */
15
+ type ConsentDecision = 'granted' | 'denied';
16
+ /**
17
+ * Consent state object for one or more categories.
18
+ *
19
+ * This is a **partial** record - you only need to specify the categories you want to set.
20
+ * Unspecified categories retain their previous state when using `updateConsent()`.
21
+ *
22
+ * @example
23
+ * ```ts
24
+ * // All four categories
25
+ * const fullState: ConsentState = {
26
+ * ad_storage: 'granted',
27
+ * analytics_storage: 'granted',
28
+ * ad_user_data: 'granted',
29
+ * ad_personalization: 'granted'
30
+ * };
31
+ *
32
+ * // Single category (partial update)
33
+ * const partialState: ConsentState = {
34
+ * analytics_storage: 'granted'
35
+ * };
36
+ *
37
+ * // Multiple specific categories
38
+ * const mixedState: ConsentState = {
39
+ * analytics_storage: 'granted',
40
+ * ad_storage: 'denied'
41
+ * };
42
+ * ```
43
+ */
44
+ type ConsentState = Partial<Record<ConsentKey, ConsentDecision>>;
45
+ interface ConsentRegionOptions {
46
+ /**
47
+ * ISO 3166-2 region codes (e.g., `US-CA`, `EEA`) that the consent command applies to.
48
+ */
49
+ region?: readonly string[];
50
+ /**
51
+ * Milliseconds to wait for an explicit update before firing tags when using the default command.
52
+ */
53
+ waitForUpdate?: number;
54
+ }
55
+ type ConsentCommand = typeof CONSENT_DEFAULT | typeof CONSENT_UPDATE;
56
+ interface ConsentCommandInput {
57
+ command: ConsentCommand;
58
+ state: ConsentState;
59
+ options?: ConsentRegionOptions;
60
+ }
61
+ type ConsentCommandValue = [typeof CONSENT_COMMAND, ConsentCommand, ConsentState] | [typeof CONSENT_COMMAND, ConsentCommand, ConsentState, Record<string, unknown>];
62
+ declare const buildConsentCommand: ({ command, state, options }: ConsentCommandInput) => ConsentCommandValue;
63
+ declare const createConsentDefaultsCommand: (state: ConsentState, options?: ConsentRegionOptions) => DataLayerValue;
64
+ declare const createConsentUpdateCommand: (state: ConsentState, options?: ConsentRegionOptions) => DataLayerValue;
65
+ declare const consent: {
66
+ buildConsentCommand: ({ command, state, options }: ConsentCommandInput) => ConsentCommandValue;
67
+ createConsentDefaultsCommand: (state: ConsentState, options?: ConsentRegionOptions) => DataLayerValue;
68
+ createConsentUpdateCommand: (state: ConsentState, options?: ConsentRegionOptions) => DataLayerValue;
69
+ normalizeConsentState: (state: ConsentState) => ConsentState;
70
+ };
71
+
72
+ type DataLayerValue = Record<string, unknown> | ((...args: unknown[]) => unknown) | unknown[];
73
+ interface Logger {
74
+ debug(message: string, details?: Record<string, unknown>): void;
75
+ info(message: string, details?: Record<string, unknown>): void;
76
+ warn(message: string, details?: Record<string, unknown>): void;
77
+ error(message: string, details?: Record<string, unknown>): void;
78
+ }
79
+ type PartialLogger = Partial<Logger>;
80
+ type ScriptAttributeValue = string | boolean | null | undefined;
81
+ interface ScriptAttributes {
82
+ async?: boolean;
83
+ defer?: boolean;
84
+ nonce?: string;
85
+ [attribute: string]: ScriptAttributeValue;
86
+ }
87
+ type ScriptLoadStatus = 'loaded' | 'failed' | 'skipped';
88
+ interface ScriptLoadState {
89
+ containerId: string;
90
+ src?: string;
91
+ status: ScriptLoadStatus;
92
+ fromCache?: boolean;
93
+ error?: string;
94
+ }
95
+ interface ContainerDescriptor {
96
+ id: string;
97
+ queryParams?: Record<string, string | number | boolean>;
98
+ }
99
+ type ContainerConfigInput = string | ContainerDescriptor;
100
+ interface CreateGtmClientOptions {
101
+ containers: ContainerConfigInput[] | ContainerConfigInput;
102
+ dataLayerName?: string;
103
+ host?: string;
104
+ defaultQueryParams?: Record<string, string | number | boolean>;
105
+ scriptAttributes?: ScriptAttributes;
106
+ logger?: PartialLogger;
107
+ }
108
+ interface GtmClient {
109
+ readonly dataLayerName: string;
110
+ init(): void;
111
+ push(value: DataLayerValue): void;
112
+ setConsentDefaults(state: ConsentState, options?: ConsentRegionOptions): void;
113
+ updateConsent(state: ConsentState, options?: ConsentRegionOptions): void;
114
+ teardown(): void;
115
+ isInitialized(): boolean;
116
+ whenReady(): Promise<ScriptLoadState[]>;
117
+ onReady(callback: (state: ScriptLoadState[]) => void): () => void;
118
+ }
119
+
120
+ declare const DEFAULT_GTM_HOST = "https://www.googletagmanager.com";
121
+ declare const DEFAULT_DATA_LAYER_NAME = "dataLayer";
122
+
123
+ /**
124
+ * Pre-configured consent state presets for common scenarios.
125
+ *
126
+ * These presets cover the most common consent patterns:
127
+ * - `eeaDefault`: All denied (GDPR compliant default)
128
+ * - `allGranted`: All granted (user accepts everything)
129
+ * - `analyticsOnly`: Mixed state (analytics only, no ads)
130
+ *
131
+ * For custom combinations, pass a partial `ConsentState` object to `updateConsent()`.
132
+ * You only need to specify the categories you want to update.
133
+ *
134
+ * @example
135
+ * ```ts
136
+ * // Use a preset
137
+ * client.updateConsent(consentPresets.allGranted);
138
+ *
139
+ * // Or create a custom state
140
+ * client.updateConsent({
141
+ * analytics_storage: 'granted',
142
+ * ad_storage: 'denied'
143
+ * });
144
+ *
145
+ * // Partial updates (only update specific categories)
146
+ * client.updateConsent({ analytics_storage: 'granted' });
147
+ * ```
148
+ */
149
+ declare const consentPresets: {
150
+ /**
151
+ * All categories denied - GDPR/EEA compliant default.
152
+ *
153
+ * Use as the initial state for regions requiring explicit opt-in consent.
154
+ * Tags will be blocked until the user grants specific permissions.
155
+ *
156
+ * | Category | State |
157
+ * |----------|-------|
158
+ * | ad_storage | denied |
159
+ * | analytics_storage | denied |
160
+ * | ad_user_data | denied |
161
+ * | ad_personalization | denied |
162
+ */
163
+ readonly eeaDefault: Readonly<{
164
+ ad_storage: "denied";
165
+ analytics_storage: "denied";
166
+ ad_user_data: "denied";
167
+ ad_personalization: "denied";
168
+ }>;
169
+ /**
170
+ * All categories granted - user accepts all tracking.
171
+ *
172
+ * Use when the user clicks "Accept All" or in regions where consent is implied.
173
+ *
174
+ * | Category | State |
175
+ * |----------|-------|
176
+ * | ad_storage | granted |
177
+ * | analytics_storage | granted |
178
+ * | ad_user_data | granted |
179
+ * | ad_personalization | granted |
180
+ */
181
+ readonly allGranted: Readonly<{
182
+ ad_storage: "granted";
183
+ analytics_storage: "granted";
184
+ ad_user_data: "granted";
185
+ ad_personalization: "granted";
186
+ }>;
187
+ /**
188
+ * Analytics allowed, advertising denied - mixed consent state.
189
+ *
190
+ * Use when the user accepts analytics/statistics but rejects advertising cookies.
191
+ * This is a common "essential + analytics" consent pattern.
192
+ *
193
+ * | Category | State |
194
+ * |----------|-------|
195
+ * | ad_storage | denied |
196
+ * | analytics_storage | granted |
197
+ * | ad_user_data | denied |
198
+ * | ad_personalization | denied |
199
+ */
200
+ readonly analyticsOnly: Readonly<{
201
+ ad_storage: "denied";
202
+ analytics_storage: "granted";
203
+ ad_user_data: "denied";
204
+ ad_personalization: "denied";
205
+ }>;
206
+ };
207
+ type ConsentPresetName = keyof typeof consentPresets;
208
+ declare const getConsentPreset: (name: ConsentPresetName) => ConsentState;
209
+
210
+ declare const createGtmClient: (options: CreateGtmClientOptions) => GtmClient;
211
+
212
+ type EventName = string;
213
+ type EventPayload = Record<string, unknown>;
214
+ type GtmEvent<TName extends EventName = EventName, TPayload extends EventPayload = EventPayload> = Readonly<{
215
+ event: TName;
216
+ } & TPayload>;
217
+ type CustomEvent<TName extends EventName, TPayload extends EventPayload = EventPayload> = GtmEvent<TName, TPayload>;
218
+ interface PageViewPayload extends EventPayload {
219
+ page_title?: string;
220
+ page_location?: string;
221
+ page_path?: string;
222
+ send_to?: string;
223
+ }
224
+ type PageViewEvent = GtmEvent<'page_view', PageViewPayload>;
225
+ interface AdsConversionPayload extends EventPayload {
226
+ send_to: string;
227
+ value?: number;
228
+ currency?: string;
229
+ transaction_id?: string;
230
+ user_data?: Record<string, unknown>;
231
+ }
232
+ type AdsConversionEvent = GtmEvent<'conversion', AdsConversionPayload>;
233
+ interface EcommerceItem extends EventPayload {
234
+ item_id?: string;
235
+ item_name?: string;
236
+ item_brand?: string;
237
+ item_category?: string;
238
+ item_category2?: string;
239
+ item_category3?: string;
240
+ item_category4?: string;
241
+ item_category5?: string;
242
+ item_variant?: string;
243
+ price?: number;
244
+ quantity?: number;
245
+ coupon?: string;
246
+ discount?: number;
247
+ }
248
+ interface EcommercePayload extends EventPayload {
249
+ affiliation?: string;
250
+ coupon?: string;
251
+ currency?: string;
252
+ items: readonly EcommerceItem[];
253
+ shipping?: number;
254
+ tax?: number;
255
+ transaction_id?: string;
256
+ value?: number;
257
+ }
258
+ type EcommerceEventName = 'add_payment_info' | 'add_shipping_info' | 'add_to_cart' | 'begin_checkout' | 'purchase' | 'refund' | 'remove_from_cart' | 'select_item' | 'select_promotion' | 'view_cart' | 'view_item' | 'view_item_list' | 'view_promotion';
259
+ type EcommerceEvent<TName extends EventName = EcommerceEventName, TExtras extends EventPayload = EventPayload> = GtmEvent<TName, {
260
+ ecommerce: EcommercePayload;
261
+ } & TExtras>;
262
+ type EventForName<TName extends EventName> = TName extends 'page_view' ? PageViewEvent : TName extends 'conversion' ? AdsConversionEvent : TName extends EcommerceEventName ? EcommerceEvent<TName> : CustomEvent<TName>;
263
+
264
+ declare const pushEvent: <TName extends string, TPayload extends EventPayload = EventPayload>(client: Pick<GtmClient, 'push'>, name: TName, payload?: TPayload) => EventForName<TName>;
265
+ interface PushEcommerceOptions<TExtras extends EventPayload = EventPayload> {
266
+ extras?: TExtras;
267
+ }
268
+ declare const pushEcommerce: <TName extends EcommerceEventName, TExtras extends EventPayload = EventPayload>(client: Pick<GtmClient, 'push'>, name: TName, ecommerce: EcommercePayload, options?: PushEcommerceOptions<TExtras>) => EcommerceEvent<TName, TExtras>;
269
+
270
+ interface NoscriptOptions {
271
+ host?: string;
272
+ defaultQueryParams?: Record<string, string | number | boolean>;
273
+ iframeAttributes?: Record<string, string | number | boolean>;
274
+ }
275
+ declare const createNoscriptMarkup: (containers: ContainerConfigInput[] | ContainerConfigInput, options?: NoscriptOptions) => string;
276
+ declare const DEFAULT_NOSCRIPT_IFRAME_ATTRIBUTES: {
277
+ [x: string]: string;
278
+ };
279
+
280
+ /**
281
+ * Auto-queue: Automatic dataLayer buffering for race condition elimination.
282
+ *
283
+ * This module provides automatic buffering of dataLayer pushes that occur before
284
+ * GTM.js loads. Events are captured, stored in order, and replayed once GTM is ready.
285
+ *
286
+ * @example
287
+ * ```ts
288
+ * // Call as early as possible (ideally inline in <head>)
289
+ * import { installAutoQueue } from '@react-gtm-kit/core';
290
+ *
291
+ * installAutoQueue(); // Start buffering immediately
292
+ *
293
+ * // Later, events pushed before GTM loads are automatically queued
294
+ * window.dataLayer.push({ event: 'early_event' }); // Buffered!
295
+ *
296
+ * // When GTM loads, all buffered events replay in order
297
+ * ```
298
+ *
299
+ * @example
300
+ * ```html
301
+ * <!-- Inline script for earliest possible buffering -->
302
+ * <script>
303
+ * // Minimal inline version for <head>
304
+ * (function(w,d,n){
305
+ * w[n]=w[n]||[];var q=[],o=w[n].push.bind(w[n]);
306
+ * w[n].push=function(){q.push(arguments);return o.apply(this,arguments)};
307
+ * w.__gtmkit_buffer=q;
308
+ * })(window,document,'dataLayer');
309
+ * </script>
310
+ * ```
311
+ */
312
+ /** Options for configuring the auto-queue behavior */
313
+ interface AutoQueueOptions {
314
+ /**
315
+ * Name of the dataLayer array. Defaults to 'dataLayer'.
316
+ */
317
+ dataLayerName?: string;
318
+ /**
319
+ * Interval in milliseconds to check if GTM has loaded.
320
+ * Lower values = faster detection, higher CPU usage.
321
+ * @default 50
322
+ */
323
+ pollInterval?: number;
324
+ /**
325
+ * Maximum time in milliseconds to wait for GTM before giving up.
326
+ * Set to 0 for unlimited waiting.
327
+ * @default 30000 (30 seconds)
328
+ */
329
+ timeout?: number;
330
+ /**
331
+ * Maximum number of events to buffer.
332
+ * Prevents memory issues if GTM never loads.
333
+ * @default 1000
334
+ */
335
+ maxBufferSize?: number;
336
+ /**
337
+ * Callback fired when the buffer is replayed.
338
+ */
339
+ onReplay?: (bufferedCount: number) => void;
340
+ /**
341
+ * Callback fired if timeout is reached before GTM loads.
342
+ */
343
+ onTimeout?: (bufferedCount: number) => void;
344
+ }
345
+ /** State of the auto-queue system */
346
+ interface AutoQueueState {
347
+ /** Whether the auto-queue is currently active */
348
+ active: boolean;
349
+ /** Number of events currently buffered */
350
+ bufferedCount: number;
351
+ /** Whether GTM has been detected as ready */
352
+ gtmReady: boolean;
353
+ /** Manually trigger replay (useful for testing) */
354
+ replay: () => void;
355
+ /** Uninstall the auto-queue and restore original push */
356
+ uninstall: () => void;
357
+ }
358
+ /**
359
+ * Installs automatic dataLayer buffering that captures events before GTM loads.
360
+ *
361
+ * Call this as early as possible in your application lifecycle, ideally before
362
+ * any other scripts that might push to the dataLayer.
363
+ *
364
+ * The auto-queue:
365
+ * 1. Creates the dataLayer if it doesn't exist
366
+ * 2. Intercepts all pushes to capture them in a buffer
367
+ * 3. Detects when GTM.js loads by watching for the 'gtm.js' event
368
+ * 4. Replays all buffered events in order once GTM is ready
369
+ * 5. Removes itself, allowing normal dataLayer operation
370
+ *
371
+ * @param options - Configuration options
372
+ * @returns State object with control methods
373
+ *
374
+ * @example
375
+ * ```ts
376
+ * const queue = installAutoQueue({
377
+ * onReplay: (count) => console.log(`Replayed ${count} buffered events`),
378
+ * onTimeout: (count) => console.warn(`GTM didn't load, ${count} events buffered`)
379
+ * });
380
+ *
381
+ * // Check state
382
+ * console.log(queue.bufferedCount); // Number of events waiting
383
+ *
384
+ * // Manual control (usually not needed)
385
+ * queue.replay(); // Force replay now
386
+ * queue.uninstall(); // Remove the interceptor
387
+ * ```
388
+ */
389
+ declare function installAutoQueue(options?: AutoQueueOptions): AutoQueueState;
390
+ /**
391
+ * Creates a minimal inline script for earliest possible buffering.
392
+ *
393
+ * This returns a script that can be embedded directly in the HTML `<head>`
394
+ * before any other scripts. It's a minimal version of installAutoQueue()
395
+ * that captures events until the full GTM Kit is loaded.
396
+ *
397
+ * @param dataLayerName - Name of the dataLayer array
398
+ * @returns Inline script string to embed in HTML
399
+ *
400
+ * @example
401
+ * ```ts
402
+ * // In your SSR template
403
+ * const inlineScript = createAutoQueueScript();
404
+ * // Output: <script>{inlineScript}</script> in <head>
405
+ * ```
406
+ */
407
+ declare function createAutoQueueScript(dataLayerName?: string): string;
408
+ /**
409
+ * Attaches to an existing inline buffer created by createAutoQueueScript().
410
+ *
411
+ * If you used the inline script in your HTML head, call this when the full
412
+ * GTM Kit loads to take over buffer management and enable replay.
413
+ *
414
+ * @param options - Configuration options
415
+ * @returns State object, or null if no inline buffer exists
416
+ *
417
+ * @example
418
+ * ```ts
419
+ * // After GTM Kit bundle loads
420
+ * const queue = attachToInlineBuffer({
421
+ * onReplay: (count) => console.log(`Replayed ${count} events`)
422
+ * });
423
+ *
424
+ * if (queue) {
425
+ * console.log(`Taking over ${queue.bufferedCount} buffered events`);
426
+ * }
427
+ * ```
428
+ */
429
+ declare function attachToInlineBuffer(options?: Omit<AutoQueueOptions, 'dataLayerName'>): AutoQueueState | null;
430
+
431
+ export { AdsConversionEvent, AdsConversionPayload, AutoQueueOptions, AutoQueueState, ConsentCommand, ConsentRegionOptions, ConsentState, ContainerConfigInput, ContainerDescriptor, CreateGtmClientOptions, CustomEvent, DEFAULT_DATA_LAYER_NAME, DEFAULT_GTM_HOST, DEFAULT_NOSCRIPT_IFRAME_ATTRIBUTES, DataLayerValue, EcommerceEvent, EcommerceEventName, EcommerceItem, EcommercePayload, EventForName, EventName, EventPayload, GtmClient, GtmEvent, NoscriptOptions, PageViewEvent, PageViewPayload, PushEcommerceOptions, ScriptAttributes, ScriptLoadState, ScriptLoadStatus, attachToInlineBuffer, buildConsentCommand, consent, consentPresets, createAutoQueueScript, createConsentDefaultsCommand, createConsentUpdateCommand, createGtmClient, createNoscriptMarkup, getConsentPreset, installAutoQueue, pushEcommerce, pushEvent };