mytart 0.2.4 → 0.4.0

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/README.md CHANGED
@@ -50,10 +50,100 @@ await analytics.page({ url: 'https://example.com/pricing', name: 'Pricing' });
50
50
 
51
51
  ### Google Analytics 4
52
52
 
53
+ GA4 supports two modes via the `appType` option:
54
+
55
+ #### Server mode (default)
56
+
57
+ Uses the [GA4 Measurement Protocol](https://developers.google.com/analytics/devguides/collection/protocol/ga4) — direct HTTP calls, no browser APIs. Use this for Node.js, API routes, serverless functions, etc.
58
+
59
+ ```typescript
60
+ {
61
+ provider: 'google-analytics',
62
+ measurementId: 'G-XXXXXXXXXX',
63
+ apiSecret: 'YOUR_SECRET',
64
+ enabled: true,
65
+ // appType defaults to 'server'
66
+ }
67
+ ```
68
+
69
+ #### Browser mode
70
+
71
+ Injects Google's official [gtag.js snippet](https://developers.google.com/tag-platform/gtagjs) into the page. Use this for client-side tracking in any framework (React, Vue, Svelte, plain HTML, etc.). No `apiSecret` needed.
72
+
73
+ ```typescript
74
+ {
75
+ provider: 'google-analytics',
76
+ measurementId: 'G-XXXXXXXXXX',
77
+ appType: 'browser',
78
+ enabled: true,
79
+ }
80
+ ```
81
+
82
+ When `appType: 'browser'` is set:
83
+
84
+ - The gtag.js script is loaded once on the first `track()`, `identify()`, or `page()` call
85
+ - All calls use the standard `gtag()` API — compatible with Google Tag Tester and Tag Assistant
86
+ - SSR-safe: silently succeeds when `window` is undefined (e.g. during server-side rendering)
87
+ - `apiSecret` is not required (and not used)
88
+
89
+ #### Google Signals (demographics)
90
+
91
+ To enable demographic data (age, gender, interests) in GA4 reports, set `signals: true`:
92
+
53
93
  ```typescript
54
- { provider: 'google-analytics', measurementId: 'G-XXXXXXXXXX', apiSecret: 'YOUR_SECRET', debug?: boolean }
94
+ {
95
+ provider: 'google-analytics',
96
+ measurementId: 'G-XXXXXXXXXX',
97
+ appType: 'browser',
98
+ enabled: true,
99
+ signals: true,
100
+ }
55
101
  ```
56
102
 
103
+ This does two things automatically:
104
+
105
+ 1. Passes `allow_google_signals: true` and `allow_ad_personalization_signals: true` to `gtag('config')`
106
+ 2. Sets Consent Mode v2 defaults granting `ad_personalization`, `ad_user_data`, `ad_storage`, and `analytics_storage`
107
+
108
+ Set `signals: false` to explicitly disable Google Signals. Omit the flag entirely to use Google's default behaviour.
109
+
110
+ > **Note**: You must also enable Google Signals in the GA4 admin panel (Admin > Data Settings > Data Collection) for demographic data to appear.
111
+
112
+ #### Consent Mode v2
113
+
114
+ For GDPR/privacy compliance you can control Consent Mode v2 directly. Use `defaultConsent` to set the initial consent state (emitted before `gtag('config')`), and `updateConsent()` to change it at runtime when the user interacts with a cookie banner.
115
+
116
+ ```typescript
117
+ const analytics = new Mytart({
118
+ providers: [{
119
+ provider: 'google-analytics',
120
+ measurementId: 'G-XXXXXXXXXX',
121
+ appType: 'browser',
122
+ enabled: true,
123
+ signals: true,
124
+ defaultConsent: {
125
+ ad_storage: 'denied',
126
+ analytics_storage: 'denied',
127
+ ad_user_data: 'denied',
128
+ ad_personalization: 'denied',
129
+ },
130
+ consentWaitForUpdate: 500, // wait 500ms for consent banner
131
+ }],
132
+ });
133
+
134
+ // After the user accepts the cookie banner:
135
+ await analytics.updateConsent({
136
+ ad_storage: 'granted',
137
+ analytics_storage: 'granted',
138
+ ad_user_data: 'granted',
139
+ ad_personalization: 'granted',
140
+ });
141
+ ```
142
+
143
+ When both `signals: true` and `defaultConsent` are set, the explicit `defaultConsent` takes precedence over the auto-consent that `signals` would generate. This lets you combine `signals: true` (for the config flags) with a GDPR-safe denied-by-default consent flow.
144
+
145
+ Consent Mode is a no-op in server mode (the Measurement Protocol does not support it).
146
+
57
147
  ### Mixpanel
58
148
 
59
149
  ```typescript
@@ -148,6 +238,22 @@ interface PageOptions {
148
238
  }
149
239
  ```
150
240
 
241
+ ### `analytics.updateConsent(consent: ConsentSettings): Promise<void>`
242
+
243
+ Updates Google Consent Mode v2 state at runtime. Call this when the user interacts with a cookie/consent banner. Only affects Google Analytics in browser mode; all other providers ignore it.
244
+
245
+ ```typescript
246
+ interface ConsentSettings {
247
+ ad_storage?: 'granted' | 'denied';
248
+ analytics_storage?: 'granted' | 'denied';
249
+ ad_user_data?: 'granted' | 'denied';
250
+ ad_personalization?: 'granted' | 'denied';
251
+ functionality_storage?: 'granted' | 'denied';
252
+ personalization_storage?: 'granted' | 'denied';
253
+ security_storage?: 'granted' | 'denied';
254
+ }
255
+ ```
256
+
151
257
  ### `analytics.addProvider(config: ProviderConfig): void`
152
258
 
153
259
  Dynamically add a provider at runtime.
@@ -187,8 +293,8 @@ All types are exported:
187
293
  ```typescript
188
294
  import type {
189
295
  MytartConfig, BaseProviderConfig, ProviderConfig, TrackOptions, IdentifyOptions, PageOptions,
190
- TrackResult, MytartError, EventContext, ProviderName,
191
- GoogleAnalyticsConfig, MixpanelConfig, SegmentConfig,
296
+ TrackResult, MytartError, EventContext, ProviderName, GoogleAnalyticsAppType,
297
+ GoogleAnalyticsConfig, ConsentSettings, ConsentState, MixpanelConfig, SegmentConfig,
192
298
  AmplitudeConfig, PlausibleConfig, PostHogConfig,
193
299
  } from 'mytart';
194
300
  ```
package/dist/index.d.mts CHANGED
@@ -8,6 +8,33 @@ interface BaseProviderConfig {
8
8
  /** Whether this provider is active. Defaults to `false` when omitted. */
9
9
  enabled?: boolean;
10
10
  }
11
+ type ConsentState = 'granted' | 'denied';
12
+ /**
13
+ * Google Consent Mode v2 settings.
14
+ * Controls how Google tags behave based on user consent.
15
+ *
16
+ * Key fields for Google Signals demographics (age, gender, interests):
17
+ * - `ad_personalization` — must be `'granted'` for Google Signals to attribute
18
+ * demographic data to sessions.
19
+ * - `ad_user_data` — must be `'granted'` for user data to be sent to Google
20
+ * for advertising purposes.
21
+ */
22
+ interface ConsentSettings {
23
+ /** Controls storage of advertising-related cookies. */
24
+ ad_storage?: ConsentState;
25
+ /** Controls storage of analytics-related cookies. */
26
+ analytics_storage?: ConsentState;
27
+ /** Controls whether user data can be sent to Google for advertising. */
28
+ ad_user_data?: ConsentState;
29
+ /** Controls whether data can be used for personalized advertising. */
30
+ ad_personalization?: ConsentState;
31
+ /** Controls storage for functional purposes (e.g. language settings). */
32
+ functionality_storage?: ConsentState;
33
+ /** Controls storage for personalization (e.g. video recommendations). */
34
+ personalization_storage?: ConsentState;
35
+ /** Controls storage for security purposes (e.g. authentication). */
36
+ security_storage?: ConsentState;
37
+ }
11
38
  type ProviderName = 'google-analytics' | 'mixpanel' | 'segment' | 'amplitude' | 'plausible' | 'posthog';
12
39
  type GoogleAnalyticsAppType = 'browser' | 'server';
13
40
  interface GoogleAnalyticsConfig extends BaseProviderConfig {
@@ -17,6 +44,44 @@ interface GoogleAnalyticsConfig extends BaseProviderConfig {
17
44
  clientId?: string;
18
45
  debug?: boolean;
19
46
  appType?: GoogleAnalyticsAppType;
47
+ /**
48
+ * Default consent state set before gtag('config'). In browser mode this
49
+ * emits `gtag('consent', 'default', ...)` so Google tags respect user
50
+ * consent from the very first hit. Use `Mytart.updateConsent()` to change
51
+ * consent at runtime (e.g. after a cookie banner interaction).
52
+ *
53
+ * For Google Signals demographics, `ad_personalization` and `ad_user_data`
54
+ * must eventually be set to `'granted'`.
55
+ */
56
+ defaultConsent?: ConsentSettings;
57
+ /**
58
+ * When `true`, sets `wait_for_update` (in milliseconds) on the default
59
+ * consent command. This tells Google tags to wait the specified number of
60
+ * milliseconds for a consent update before sending the first hit. Useful
61
+ * when a consent management platform loads asynchronously.
62
+ * Defaults to `undefined` (no wait).
63
+ */
64
+ consentWaitForUpdate?: number;
65
+ /**
66
+ * Convenience flag to enable or disable Google Signals for demographics
67
+ * (age, gender, interests).
68
+ *
69
+ * - `true` — passes `allow_google_signals: true` and
70
+ * `allow_ad_personalization_signals: true` in the `gtag('config')`
71
+ * call. If `defaultConsent` is not explicitly set, automatically
72
+ * configures Consent Mode v2 to grant `ad_personalization`,
73
+ * `ad_user_data`, `ad_storage`, and `analytics_storage`.
74
+ * - `false` — passes `allow_google_signals: false` and
75
+ * `allow_ad_personalization_signals: false` to explicitly disable
76
+ * Google Signals.
77
+ * - `undefined` (default) — uses Google's default behaviour (Signals
78
+ * enabled when the GA4 property has it turned on in Admin).
79
+ *
80
+ * **Note**: Google Signals must also be enabled in the GA4 admin panel
81
+ * (Admin › Data Settings › Data Collection) for demographic data to
82
+ * appear. This flag controls the client-side consent and config only.
83
+ */
84
+ signals?: boolean;
20
85
  }
21
86
  interface MixpanelConfig extends BaseProviderConfig {
22
87
  provider: 'mixpanel';
@@ -106,6 +171,16 @@ declare class Mytart {
106
171
  track(options: TrackOptions): Promise<TrackResult[]>;
107
172
  identify(options: IdentifyOptions): Promise<TrackResult[]>;
108
173
  page(options: PageOptions): Promise<TrackResult[]>;
174
+ /**
175
+ * Update consent state across all providers that support consent management.
176
+ * Currently this is meaningful for Google Analytics (Consent Mode v2) in
177
+ * browser mode. Call this when the user interacts with a cookie/consent
178
+ * banner.
179
+ *
180
+ * To enable Google Signals demographics (age, gender, interests), grant
181
+ * at least `ad_personalization` and `ad_user_data`.
182
+ */
183
+ updateConsent(consent: ConsentSettings): Promise<void>;
109
184
  addProvider(config: ProviderConfig): void;
110
185
  removeProvider(name: string): void;
111
186
  getProviders(): string[];
@@ -116,6 +191,11 @@ declare abstract class BaseProvider {
116
191
  abstract track(options: TrackOptions): Promise<TrackResult>;
117
192
  abstract identify(options: IdentifyOptions): Promise<TrackResult>;
118
193
  abstract page(options: PageOptions): Promise<TrackResult>;
194
+ /**
195
+ * Update consent state. Only meaningful for providers that support consent
196
+ * management (e.g. Google Analytics Consent Mode v2). Default is a no-op.
197
+ */
198
+ updateConsent(_consent: ConsentSettings): Promise<void>;
119
199
  protected buildError(message: string, code: string, originalError?: unknown): TrackResult;
120
200
  protected buildSuccess(statusCode?: number): TrackResult;
121
201
  }
@@ -126,34 +206,60 @@ declare global {
126
206
  dataLayer: unknown[];
127
207
  }
128
208
  }
129
- interface GtagFn {
130
- (command: 'config', measurementId: string, config?: GtagConfig): void;
131
- (command: 'event', eventName: string, params?: Record<string, unknown>): void;
132
- (command: 'set', config: Record<string, unknown>): void;
133
- (command: string, ...args: unknown[]): void;
134
- }
135
- interface GtagConfig {
136
- send_page_view?: boolean;
137
- page_title?: string;
138
- page_location?: string;
139
- page_referrer?: string;
140
- user_id?: string;
141
- [key: string]: unknown;
142
- }
209
+ type GtagFn = (...args: unknown[]) => void;
143
210
  declare class GoogleAnalyticsProvider extends BaseProvider {
144
211
  readonly name = "google-analytics";
145
212
  private readonly config;
146
213
  private readonly http;
147
214
  private readonly endpoint;
148
215
  private readonly isBrowser;
149
- private gtagInitialized;
216
+ private gtagReady;
150
217
  constructor(config: GoogleAnalyticsConfig);
151
- private ensureGtagLoaded;
152
- private injectGtagScript;
153
- private buildGtagResult;
218
+ /**
219
+ * Initializes the gtag.js snippet exactly as Google's official documentation
220
+ * specifies. This mirrors the standard snippet:
221
+ *
222
+ * <script async src="https://www.googletagmanager.com/gtag/js?id=TAG_ID"></script>
223
+ * <script>
224
+ * window.dataLayer = window.dataLayer || [];
225
+ * function gtag(){dataLayer.push(arguments);}
226
+ * gtag('js', new Date());
227
+ * gtag('config', 'TAG_ID');
228
+ * </script>
229
+ *
230
+ * When `defaultConsent` is configured, a `gtag('consent', 'default', ...)`
231
+ * call is emitted **before** `gtag('js')` and `gtag('config')`, as required
232
+ * by Google's Consent Mode v2 specification.
233
+ *
234
+ * Key details:
235
+ * - The script URL MUST include ?id=TAG_ID for Google Tag Tester detection
236
+ * - dataLayer and the gtag shim are set up BEFORE the script loads
237
+ * - gtag('js') and gtag('config') are called synchronously — they queue
238
+ * into dataLayer and are processed once the real script loads
239
+ * - The returned promise resolves when the script finishes loading
240
+ */
241
+ private initGtag;
242
+ /**
243
+ * Ensures gtag is initialized exactly once. Subsequent calls return the
244
+ * same promise so the script is never injected twice.
245
+ */
246
+ private ensureGtag;
154
247
  private trackBrowser;
155
248
  private identifyBrowser;
156
249
  private pageBrowser;
250
+ private buildGtagResult;
251
+ /**
252
+ * Updates the consent state at runtime. In browser mode this emits
253
+ * `gtag('consent', 'update', ...)`. Call this when the user interacts
254
+ * with a cookie/consent banner.
255
+ *
256
+ * To enable Google Signals demographics (age, gender, interests), grant
257
+ * at least `ad_personalization` and `ad_user_data`.
258
+ *
259
+ * In server mode this is a no-op — the Measurement Protocol does not
260
+ * support Consent Mode.
261
+ */
262
+ updateConsent(consent: ConsentSettings): Promise<void>;
157
263
  track({ event, properties, userId, anonymousId, timestamp }: TrackOptions): Promise<TrackResult>;
158
264
  identify({ userId, traits }: IdentifyOptions): Promise<TrackResult>;
159
265
  page({ name, url, referrer, userId, anonymousId }: PageOptions): Promise<TrackResult>;
@@ -216,4 +322,4 @@ declare class PostHogProvider extends BaseProvider {
216
322
  page({ name, url, userId, anonymousId, properties }: PageOptions): Promise<TrackResult>;
217
323
  }
218
324
 
219
- export { type AmplitudeConfig, AmplitudeProvider, BaseProvider, type BaseProviderConfig, type EventContext, type GoogleAnalyticsAppType, type GoogleAnalyticsConfig, GoogleAnalyticsProvider, type IdentifyOptions, type MixpanelConfig, MixpanelProvider, Mytart, type MytartConfig, type MytartError, type PageOptions, type PlausibleConfig, PlausibleProvider, type PostHogConfig, PostHogProvider, type ProviderConfig, type ProviderName, type SegmentConfig, SegmentProvider, type TrackOptions, type TrackResult };
325
+ export { type AmplitudeConfig, AmplitudeProvider, BaseProvider, type BaseProviderConfig, type ConsentSettings, type ConsentState, type EventContext, type GoogleAnalyticsAppType, type GoogleAnalyticsConfig, GoogleAnalyticsProvider, type IdentifyOptions, type MixpanelConfig, MixpanelProvider, Mytart, type MytartConfig, type MytartError, type PageOptions, type PlausibleConfig, PlausibleProvider, type PostHogConfig, PostHogProvider, type ProviderConfig, type ProviderName, type SegmentConfig, SegmentProvider, type TrackOptions, type TrackResult };
package/dist/index.d.ts CHANGED
@@ -8,6 +8,33 @@ interface BaseProviderConfig {
8
8
  /** Whether this provider is active. Defaults to `false` when omitted. */
9
9
  enabled?: boolean;
10
10
  }
11
+ type ConsentState = 'granted' | 'denied';
12
+ /**
13
+ * Google Consent Mode v2 settings.
14
+ * Controls how Google tags behave based on user consent.
15
+ *
16
+ * Key fields for Google Signals demographics (age, gender, interests):
17
+ * - `ad_personalization` — must be `'granted'` for Google Signals to attribute
18
+ * demographic data to sessions.
19
+ * - `ad_user_data` — must be `'granted'` for user data to be sent to Google
20
+ * for advertising purposes.
21
+ */
22
+ interface ConsentSettings {
23
+ /** Controls storage of advertising-related cookies. */
24
+ ad_storage?: ConsentState;
25
+ /** Controls storage of analytics-related cookies. */
26
+ analytics_storage?: ConsentState;
27
+ /** Controls whether user data can be sent to Google for advertising. */
28
+ ad_user_data?: ConsentState;
29
+ /** Controls whether data can be used for personalized advertising. */
30
+ ad_personalization?: ConsentState;
31
+ /** Controls storage for functional purposes (e.g. language settings). */
32
+ functionality_storage?: ConsentState;
33
+ /** Controls storage for personalization (e.g. video recommendations). */
34
+ personalization_storage?: ConsentState;
35
+ /** Controls storage for security purposes (e.g. authentication). */
36
+ security_storage?: ConsentState;
37
+ }
11
38
  type ProviderName = 'google-analytics' | 'mixpanel' | 'segment' | 'amplitude' | 'plausible' | 'posthog';
12
39
  type GoogleAnalyticsAppType = 'browser' | 'server';
13
40
  interface GoogleAnalyticsConfig extends BaseProviderConfig {
@@ -17,6 +44,44 @@ interface GoogleAnalyticsConfig extends BaseProviderConfig {
17
44
  clientId?: string;
18
45
  debug?: boolean;
19
46
  appType?: GoogleAnalyticsAppType;
47
+ /**
48
+ * Default consent state set before gtag('config'). In browser mode this
49
+ * emits `gtag('consent', 'default', ...)` so Google tags respect user
50
+ * consent from the very first hit. Use `Mytart.updateConsent()` to change
51
+ * consent at runtime (e.g. after a cookie banner interaction).
52
+ *
53
+ * For Google Signals demographics, `ad_personalization` and `ad_user_data`
54
+ * must eventually be set to `'granted'`.
55
+ */
56
+ defaultConsent?: ConsentSettings;
57
+ /**
58
+ * When `true`, sets `wait_for_update` (in milliseconds) on the default
59
+ * consent command. This tells Google tags to wait the specified number of
60
+ * milliseconds for a consent update before sending the first hit. Useful
61
+ * when a consent management platform loads asynchronously.
62
+ * Defaults to `undefined` (no wait).
63
+ */
64
+ consentWaitForUpdate?: number;
65
+ /**
66
+ * Convenience flag to enable or disable Google Signals for demographics
67
+ * (age, gender, interests).
68
+ *
69
+ * - `true` — passes `allow_google_signals: true` and
70
+ * `allow_ad_personalization_signals: true` in the `gtag('config')`
71
+ * call. If `defaultConsent` is not explicitly set, automatically
72
+ * configures Consent Mode v2 to grant `ad_personalization`,
73
+ * `ad_user_data`, `ad_storage`, and `analytics_storage`.
74
+ * - `false` — passes `allow_google_signals: false` and
75
+ * `allow_ad_personalization_signals: false` to explicitly disable
76
+ * Google Signals.
77
+ * - `undefined` (default) — uses Google's default behaviour (Signals
78
+ * enabled when the GA4 property has it turned on in Admin).
79
+ *
80
+ * **Note**: Google Signals must also be enabled in the GA4 admin panel
81
+ * (Admin › Data Settings › Data Collection) for demographic data to
82
+ * appear. This flag controls the client-side consent and config only.
83
+ */
84
+ signals?: boolean;
20
85
  }
21
86
  interface MixpanelConfig extends BaseProviderConfig {
22
87
  provider: 'mixpanel';
@@ -106,6 +171,16 @@ declare class Mytart {
106
171
  track(options: TrackOptions): Promise<TrackResult[]>;
107
172
  identify(options: IdentifyOptions): Promise<TrackResult[]>;
108
173
  page(options: PageOptions): Promise<TrackResult[]>;
174
+ /**
175
+ * Update consent state across all providers that support consent management.
176
+ * Currently this is meaningful for Google Analytics (Consent Mode v2) in
177
+ * browser mode. Call this when the user interacts with a cookie/consent
178
+ * banner.
179
+ *
180
+ * To enable Google Signals demographics (age, gender, interests), grant
181
+ * at least `ad_personalization` and `ad_user_data`.
182
+ */
183
+ updateConsent(consent: ConsentSettings): Promise<void>;
109
184
  addProvider(config: ProviderConfig): void;
110
185
  removeProvider(name: string): void;
111
186
  getProviders(): string[];
@@ -116,6 +191,11 @@ declare abstract class BaseProvider {
116
191
  abstract track(options: TrackOptions): Promise<TrackResult>;
117
192
  abstract identify(options: IdentifyOptions): Promise<TrackResult>;
118
193
  abstract page(options: PageOptions): Promise<TrackResult>;
194
+ /**
195
+ * Update consent state. Only meaningful for providers that support consent
196
+ * management (e.g. Google Analytics Consent Mode v2). Default is a no-op.
197
+ */
198
+ updateConsent(_consent: ConsentSettings): Promise<void>;
119
199
  protected buildError(message: string, code: string, originalError?: unknown): TrackResult;
120
200
  protected buildSuccess(statusCode?: number): TrackResult;
121
201
  }
@@ -126,34 +206,60 @@ declare global {
126
206
  dataLayer: unknown[];
127
207
  }
128
208
  }
129
- interface GtagFn {
130
- (command: 'config', measurementId: string, config?: GtagConfig): void;
131
- (command: 'event', eventName: string, params?: Record<string, unknown>): void;
132
- (command: 'set', config: Record<string, unknown>): void;
133
- (command: string, ...args: unknown[]): void;
134
- }
135
- interface GtagConfig {
136
- send_page_view?: boolean;
137
- page_title?: string;
138
- page_location?: string;
139
- page_referrer?: string;
140
- user_id?: string;
141
- [key: string]: unknown;
142
- }
209
+ type GtagFn = (...args: unknown[]) => void;
143
210
  declare class GoogleAnalyticsProvider extends BaseProvider {
144
211
  readonly name = "google-analytics";
145
212
  private readonly config;
146
213
  private readonly http;
147
214
  private readonly endpoint;
148
215
  private readonly isBrowser;
149
- private gtagInitialized;
216
+ private gtagReady;
150
217
  constructor(config: GoogleAnalyticsConfig);
151
- private ensureGtagLoaded;
152
- private injectGtagScript;
153
- private buildGtagResult;
218
+ /**
219
+ * Initializes the gtag.js snippet exactly as Google's official documentation
220
+ * specifies. This mirrors the standard snippet:
221
+ *
222
+ * <script async src="https://www.googletagmanager.com/gtag/js?id=TAG_ID"></script>
223
+ * <script>
224
+ * window.dataLayer = window.dataLayer || [];
225
+ * function gtag(){dataLayer.push(arguments);}
226
+ * gtag('js', new Date());
227
+ * gtag('config', 'TAG_ID');
228
+ * </script>
229
+ *
230
+ * When `defaultConsent` is configured, a `gtag('consent', 'default', ...)`
231
+ * call is emitted **before** `gtag('js')` and `gtag('config')`, as required
232
+ * by Google's Consent Mode v2 specification.
233
+ *
234
+ * Key details:
235
+ * - The script URL MUST include ?id=TAG_ID for Google Tag Tester detection
236
+ * - dataLayer and the gtag shim are set up BEFORE the script loads
237
+ * - gtag('js') and gtag('config') are called synchronously — they queue
238
+ * into dataLayer and are processed once the real script loads
239
+ * - The returned promise resolves when the script finishes loading
240
+ */
241
+ private initGtag;
242
+ /**
243
+ * Ensures gtag is initialized exactly once. Subsequent calls return the
244
+ * same promise so the script is never injected twice.
245
+ */
246
+ private ensureGtag;
154
247
  private trackBrowser;
155
248
  private identifyBrowser;
156
249
  private pageBrowser;
250
+ private buildGtagResult;
251
+ /**
252
+ * Updates the consent state at runtime. In browser mode this emits
253
+ * `gtag('consent', 'update', ...)`. Call this when the user interacts
254
+ * with a cookie/consent banner.
255
+ *
256
+ * To enable Google Signals demographics (age, gender, interests), grant
257
+ * at least `ad_personalization` and `ad_user_data`.
258
+ *
259
+ * In server mode this is a no-op — the Measurement Protocol does not
260
+ * support Consent Mode.
261
+ */
262
+ updateConsent(consent: ConsentSettings): Promise<void>;
157
263
  track({ event, properties, userId, anonymousId, timestamp }: TrackOptions): Promise<TrackResult>;
158
264
  identify({ userId, traits }: IdentifyOptions): Promise<TrackResult>;
159
265
  page({ name, url, referrer, userId, anonymousId }: PageOptions): Promise<TrackResult>;
@@ -216,4 +322,4 @@ declare class PostHogProvider extends BaseProvider {
216
322
  page({ name, url, userId, anonymousId, properties }: PageOptions): Promise<TrackResult>;
217
323
  }
218
324
 
219
- export { type AmplitudeConfig, AmplitudeProvider, BaseProvider, type BaseProviderConfig, type EventContext, type GoogleAnalyticsAppType, type GoogleAnalyticsConfig, GoogleAnalyticsProvider, type IdentifyOptions, type MixpanelConfig, MixpanelProvider, Mytart, type MytartConfig, type MytartError, type PageOptions, type PlausibleConfig, PlausibleProvider, type PostHogConfig, PostHogProvider, type ProviderConfig, type ProviderName, type SegmentConfig, SegmentProvider, type TrackOptions, type TrackResult };
325
+ export { type AmplitudeConfig, AmplitudeProvider, BaseProvider, type BaseProviderConfig, type ConsentSettings, type ConsentState, type EventContext, type GoogleAnalyticsAppType, type GoogleAnalyticsConfig, GoogleAnalyticsProvider, type IdentifyOptions, type MixpanelConfig, MixpanelProvider, Mytart, type MytartConfig, type MytartError, type PageOptions, type PlausibleConfig, PlausibleProvider, type PostHogConfig, PostHogProvider, type ProviderConfig, type ProviderName, type SegmentConfig, SegmentProvider, type TrackOptions, type TrackResult };
package/dist/index.js CHANGED
@@ -43,6 +43,13 @@ module.exports = __toCommonJS(index_exports);
43
43
 
44
44
  // src/providers/base.ts
45
45
  var BaseProvider = class {
46
+ /**
47
+ * Update consent state. Only meaningful for providers that support consent
48
+ * management (e.g. Google Analytics Consent Mode v2). Default is a no-op.
49
+ */
50
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
51
+ async updateConsent(_consent) {
52
+ }
46
53
  buildError(message, code, originalError) {
47
54
  return {
48
55
  provider: this.name,
@@ -82,75 +89,116 @@ function isAxiosError(error) {
82
89
  // src/providers/google-analytics.ts
83
90
  var GA4_ENDPOINT = "https://www.google-analytics.com/mp/collect";
84
91
  var GA4_DEBUG_ENDPOINT = "https://www.google-analytics.com/debug/mp/collect";
85
- var GTAG_URL = "https://www.googletagmanager.com/gtag/js";
92
+ var GTAG_SCRIPT_URL = "https://www.googletagmanager.com/gtag/js";
86
93
  var GoogleAnalyticsProvider = class extends BaseProvider {
87
94
  constructor(config) {
88
95
  super();
89
96
  this.name = "google-analytics";
90
- this.gtagInitialized = false;
97
+ this.gtagReady = null;
91
98
  this.config = config;
92
99
  this.http = createHttpClient();
93
100
  this.endpoint = config.debug ? GA4_DEBUG_ENDPOINT : GA4_ENDPOINT;
94
101
  this.isBrowser = config.appType === "browser";
95
102
  }
96
- async ensureGtagLoaded() {
97
- if (typeof window === "undefined") {
98
- return;
103
+ /**
104
+ * Initializes the gtag.js snippet exactly as Google's official documentation
105
+ * specifies. This mirrors the standard snippet:
106
+ *
107
+ * <script async src="https://www.googletagmanager.com/gtag/js?id=TAG_ID"></script>
108
+ * <script>
109
+ * window.dataLayer = window.dataLayer || [];
110
+ * function gtag(){dataLayer.push(arguments);}
111
+ * gtag('js', new Date());
112
+ * gtag('config', 'TAG_ID');
113
+ * </script>
114
+ *
115
+ * When `defaultConsent` is configured, a `gtag('consent', 'default', ...)`
116
+ * call is emitted **before** `gtag('js')` and `gtag('config')`, as required
117
+ * by Google's Consent Mode v2 specification.
118
+ *
119
+ * Key details:
120
+ * - The script URL MUST include ?id=TAG_ID for Google Tag Tester detection
121
+ * - dataLayer and the gtag shim are set up BEFORE the script loads
122
+ * - gtag('js') and gtag('config') are called synchronously — they queue
123
+ * into dataLayer and are processed once the real script loads
124
+ * - The returned promise resolves when the script finishes loading
125
+ */
126
+ initGtag() {
127
+ if (typeof window === "undefined" || typeof document === "undefined") {
128
+ return Promise.resolve();
99
129
  }
100
- if (typeof window.gtag === "function") {
101
- return;
130
+ window.dataLayer = window.dataLayer || [];
131
+ window.gtag = function gtag() {
132
+ window.dataLayer.push(arguments);
133
+ };
134
+ const resolvedConsent = this.config.defaultConsent ?? (this.config.signals === true ? {
135
+ ad_storage: "granted",
136
+ analytics_storage: "granted",
137
+ ad_user_data: "granted",
138
+ ad_personalization: "granted"
139
+ } : void 0);
140
+ if (resolvedConsent) {
141
+ const consentParams = { ...resolvedConsent };
142
+ if (this.config.consentWaitForUpdate !== void 0) {
143
+ consentParams["wait_for_update"] = this.config.consentWaitForUpdate;
144
+ }
145
+ window.gtag("consent", "default", consentParams);
146
+ }
147
+ window.gtag("js", /* @__PURE__ */ new Date());
148
+ const configParams = {};
149
+ if (this.config.signals === true) {
150
+ configParams["allow_google_signals"] = true;
151
+ configParams["allow_ad_personalization_signals"] = true;
152
+ } else if (this.config.signals === false) {
153
+ configParams["allow_google_signals"] = false;
154
+ configParams["allow_ad_personalization_signals"] = false;
155
+ }
156
+ if (Object.keys(configParams).length > 0) {
157
+ window.gtag("config", this.config.measurementId, configParams);
158
+ } else {
159
+ window.gtag("config", this.config.measurementId);
102
160
  }
103
- await this.injectGtagScript();
104
- }
105
- injectGtagScript() {
106
161
  return new Promise((resolve) => {
107
- window.dataLayer = window.dataLayer || [];
108
- window.gtag = window.gtag || function gtag(...args) {
109
- window.dataLayer.push(args);
110
- };
111
162
  const script = document.createElement("script");
112
163
  script.async = true;
113
- script.src = GTAG_URL;
114
- script.onload = () => {
115
- window.gtag("js", /* @__PURE__ */ new Date());
116
- window.gtag("config", this.config.measurementId, {
117
- send_page_view: false
118
- });
119
- resolve();
120
- };
164
+ script.src = `${GTAG_SCRIPT_URL}?id=${this.config.measurementId}`;
165
+ script.onload = () => resolve();
121
166
  script.onerror = () => resolve();
122
- const firstScript = document.getElementsByTagName("script")[0];
123
- firstScript.parentNode?.insertBefore(script, firstScript);
167
+ document.head.appendChild(script);
124
168
  });
125
169
  }
126
- buildGtagResult() {
127
- return {
128
- provider: this.name,
129
- success: true,
130
- statusCode: 200
131
- };
170
+ /**
171
+ * Ensures gtag is initialized exactly once. Subsequent calls return the
172
+ * same promise so the script is never injected twice.
173
+ */
174
+ ensureGtag() {
175
+ if (!this.gtagReady) {
176
+ this.gtagReady = this.initGtag();
177
+ }
178
+ return this.gtagReady;
132
179
  }
180
+ // ---------------------------------------------------------------------------
181
+ // Browser mode methods — delegate to gtag()
182
+ // ---------------------------------------------------------------------------
133
183
  async trackBrowser({ event, properties, userId }) {
134
184
  if (typeof window === "undefined") {
135
185
  return this.buildGtagResult();
136
186
  }
137
- await this.ensureGtagLoaded();
187
+ await this.ensureGtag();
138
188
  if (userId) {
139
189
  window.gtag("set", { user_id: userId });
140
190
  }
141
- window.gtag("event", event, properties || {});
191
+ window.gtag("event", event, properties ?? {});
142
192
  return this.buildGtagResult();
143
193
  }
144
194
  async identifyBrowser({ userId, traits }) {
145
195
  if (typeof window === "undefined") {
146
196
  return this.buildGtagResult();
147
197
  }
148
- await this.ensureGtagLoaded();
198
+ await this.ensureGtag();
149
199
  window.gtag("set", { user_id: userId });
150
200
  if (traits) {
151
- Object.entries(traits).forEach(([key, value]) => {
152
- window.gtag("set", { [key]: value });
153
- });
201
+ window.gtag("set", "user_properties", traits);
154
202
  }
155
203
  return this.buildGtagResult();
156
204
  }
@@ -158,31 +206,60 @@ var GoogleAnalyticsProvider = class extends BaseProvider {
158
206
  if (typeof window === "undefined") {
159
207
  return this.buildGtagResult();
160
208
  }
161
- await this.ensureGtagLoaded();
162
- const config = {
209
+ await this.ensureGtag();
210
+ if (userId) {
211
+ window.gtag("set", { user_id: userId });
212
+ }
213
+ window.gtag("event", "page_view", {
163
214
  page_title: name,
164
215
  page_location: url,
165
- page_referrer: referrer,
166
- send_page_view: true
216
+ page_referrer: referrer
217
+ });
218
+ return this.buildGtagResult();
219
+ }
220
+ buildGtagResult() {
221
+ return {
222
+ provider: this.name,
223
+ success: true,
224
+ statusCode: 200
167
225
  };
168
- if (userId) {
169
- config.user_id = userId;
226
+ }
227
+ // ---------------------------------------------------------------------------
228
+ // Consent Mode v2
229
+ // ---------------------------------------------------------------------------
230
+ /**
231
+ * Updates the consent state at runtime. In browser mode this emits
232
+ * `gtag('consent', 'update', ...)`. Call this when the user interacts
233
+ * with a cookie/consent banner.
234
+ *
235
+ * To enable Google Signals demographics (age, gender, interests), grant
236
+ * at least `ad_personalization` and `ad_user_data`.
237
+ *
238
+ * In server mode this is a no-op — the Measurement Protocol does not
239
+ * support Consent Mode.
240
+ */
241
+ async updateConsent(consent) {
242
+ if (!this.isBrowser || typeof window === "undefined") {
243
+ return;
170
244
  }
171
- window.gtag("config", this.config.measurementId, config);
172
- return this.buildGtagResult();
245
+ await this.ensureGtag();
246
+ window.gtag("consent", "update", consent);
173
247
  }
248
+ // ---------------------------------------------------------------------------
249
+ // Public API — routes to browser or server mode
250
+ // ---------------------------------------------------------------------------
174
251
  async track({ event, properties, userId, anonymousId, timestamp }) {
175
252
  if (this.isBrowser) {
176
253
  return this.trackBrowser({ event, properties, userId, anonymousId, timestamp });
177
254
  }
178
255
  try {
179
- const client_id = anonymousId ?? this.config.clientId ?? "anonymous";
256
+ const clientId = anonymousId ?? this.config.clientId ?? "anonymous";
180
257
  const params = { ...properties };
181
258
  if (timestamp) {
182
259
  params["timestamp_micros"] = timestamp.getTime() * 1e3;
183
260
  }
184
261
  const body = {
185
- client_id,
262
+ client_id: clientId,
186
263
  events: [{ name: event, params }]
187
264
  };
188
265
  if (userId) {
@@ -728,6 +805,18 @@ var Mytart = class {
728
805
  };
729
806
  return Promise.all(this.providers.map((p) => p.page(enriched)));
730
807
  }
808
+ /**
809
+ * Update consent state across all providers that support consent management.
810
+ * Currently this is meaningful for Google Analytics (Consent Mode v2) in
811
+ * browser mode. Call this when the user interacts with a cookie/consent
812
+ * banner.
813
+ *
814
+ * To enable Google Signals demographics (age, gender, interests), grant
815
+ * at least `ad_personalization` and `ad_user_data`.
816
+ */
817
+ async updateConsent(consent) {
818
+ await Promise.all(this.providers.map((p) => p.updateConsent(consent)));
819
+ }
731
820
  addProvider(config) {
732
821
  if (config.enabled !== true) return;
733
822
  this.providers.push(createProvider(config));
package/dist/index.mjs CHANGED
@@ -1,5 +1,12 @@
1
1
  // src/providers/base.ts
2
2
  var BaseProvider = class {
3
+ /**
4
+ * Update consent state. Only meaningful for providers that support consent
5
+ * management (e.g. Google Analytics Consent Mode v2). Default is a no-op.
6
+ */
7
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
8
+ async updateConsent(_consent) {
9
+ }
3
10
  buildError(message, code, originalError) {
4
11
  return {
5
12
  provider: this.name,
@@ -39,75 +46,116 @@ function isAxiosError(error) {
39
46
  // src/providers/google-analytics.ts
40
47
  var GA4_ENDPOINT = "https://www.google-analytics.com/mp/collect";
41
48
  var GA4_DEBUG_ENDPOINT = "https://www.google-analytics.com/debug/mp/collect";
42
- var GTAG_URL = "https://www.googletagmanager.com/gtag/js";
49
+ var GTAG_SCRIPT_URL = "https://www.googletagmanager.com/gtag/js";
43
50
  var GoogleAnalyticsProvider = class extends BaseProvider {
44
51
  constructor(config) {
45
52
  super();
46
53
  this.name = "google-analytics";
47
- this.gtagInitialized = false;
54
+ this.gtagReady = null;
48
55
  this.config = config;
49
56
  this.http = createHttpClient();
50
57
  this.endpoint = config.debug ? GA4_DEBUG_ENDPOINT : GA4_ENDPOINT;
51
58
  this.isBrowser = config.appType === "browser";
52
59
  }
53
- async ensureGtagLoaded() {
54
- if (typeof window === "undefined") {
55
- return;
60
+ /**
61
+ * Initializes the gtag.js snippet exactly as Google's official documentation
62
+ * specifies. This mirrors the standard snippet:
63
+ *
64
+ * <script async src="https://www.googletagmanager.com/gtag/js?id=TAG_ID"></script>
65
+ * <script>
66
+ * window.dataLayer = window.dataLayer || [];
67
+ * function gtag(){dataLayer.push(arguments);}
68
+ * gtag('js', new Date());
69
+ * gtag('config', 'TAG_ID');
70
+ * </script>
71
+ *
72
+ * When `defaultConsent` is configured, a `gtag('consent', 'default', ...)`
73
+ * call is emitted **before** `gtag('js')` and `gtag('config')`, as required
74
+ * by Google's Consent Mode v2 specification.
75
+ *
76
+ * Key details:
77
+ * - The script URL MUST include ?id=TAG_ID for Google Tag Tester detection
78
+ * - dataLayer and the gtag shim are set up BEFORE the script loads
79
+ * - gtag('js') and gtag('config') are called synchronously — they queue
80
+ * into dataLayer and are processed once the real script loads
81
+ * - The returned promise resolves when the script finishes loading
82
+ */
83
+ initGtag() {
84
+ if (typeof window === "undefined" || typeof document === "undefined") {
85
+ return Promise.resolve();
56
86
  }
57
- if (typeof window.gtag === "function") {
58
- return;
87
+ window.dataLayer = window.dataLayer || [];
88
+ window.gtag = function gtag() {
89
+ window.dataLayer.push(arguments);
90
+ };
91
+ const resolvedConsent = this.config.defaultConsent ?? (this.config.signals === true ? {
92
+ ad_storage: "granted",
93
+ analytics_storage: "granted",
94
+ ad_user_data: "granted",
95
+ ad_personalization: "granted"
96
+ } : void 0);
97
+ if (resolvedConsent) {
98
+ const consentParams = { ...resolvedConsent };
99
+ if (this.config.consentWaitForUpdate !== void 0) {
100
+ consentParams["wait_for_update"] = this.config.consentWaitForUpdate;
101
+ }
102
+ window.gtag("consent", "default", consentParams);
103
+ }
104
+ window.gtag("js", /* @__PURE__ */ new Date());
105
+ const configParams = {};
106
+ if (this.config.signals === true) {
107
+ configParams["allow_google_signals"] = true;
108
+ configParams["allow_ad_personalization_signals"] = true;
109
+ } else if (this.config.signals === false) {
110
+ configParams["allow_google_signals"] = false;
111
+ configParams["allow_ad_personalization_signals"] = false;
112
+ }
113
+ if (Object.keys(configParams).length > 0) {
114
+ window.gtag("config", this.config.measurementId, configParams);
115
+ } else {
116
+ window.gtag("config", this.config.measurementId);
59
117
  }
60
- await this.injectGtagScript();
61
- }
62
- injectGtagScript() {
63
118
  return new Promise((resolve) => {
64
- window.dataLayer = window.dataLayer || [];
65
- window.gtag = window.gtag || function gtag(...args) {
66
- window.dataLayer.push(args);
67
- };
68
119
  const script = document.createElement("script");
69
120
  script.async = true;
70
- script.src = GTAG_URL;
71
- script.onload = () => {
72
- window.gtag("js", /* @__PURE__ */ new Date());
73
- window.gtag("config", this.config.measurementId, {
74
- send_page_view: false
75
- });
76
- resolve();
77
- };
121
+ script.src = `${GTAG_SCRIPT_URL}?id=${this.config.measurementId}`;
122
+ script.onload = () => resolve();
78
123
  script.onerror = () => resolve();
79
- const firstScript = document.getElementsByTagName("script")[0];
80
- firstScript.parentNode?.insertBefore(script, firstScript);
124
+ document.head.appendChild(script);
81
125
  });
82
126
  }
83
- buildGtagResult() {
84
- return {
85
- provider: this.name,
86
- success: true,
87
- statusCode: 200
88
- };
127
+ /**
128
+ * Ensures gtag is initialized exactly once. Subsequent calls return the
129
+ * same promise so the script is never injected twice.
130
+ */
131
+ ensureGtag() {
132
+ if (!this.gtagReady) {
133
+ this.gtagReady = this.initGtag();
134
+ }
135
+ return this.gtagReady;
89
136
  }
137
+ // ---------------------------------------------------------------------------
138
+ // Browser mode methods — delegate to gtag()
139
+ // ---------------------------------------------------------------------------
90
140
  async trackBrowser({ event, properties, userId }) {
91
141
  if (typeof window === "undefined") {
92
142
  return this.buildGtagResult();
93
143
  }
94
- await this.ensureGtagLoaded();
144
+ await this.ensureGtag();
95
145
  if (userId) {
96
146
  window.gtag("set", { user_id: userId });
97
147
  }
98
- window.gtag("event", event, properties || {});
148
+ window.gtag("event", event, properties ?? {});
99
149
  return this.buildGtagResult();
100
150
  }
101
151
  async identifyBrowser({ userId, traits }) {
102
152
  if (typeof window === "undefined") {
103
153
  return this.buildGtagResult();
104
154
  }
105
- await this.ensureGtagLoaded();
155
+ await this.ensureGtag();
106
156
  window.gtag("set", { user_id: userId });
107
157
  if (traits) {
108
- Object.entries(traits).forEach(([key, value]) => {
109
- window.gtag("set", { [key]: value });
110
- });
158
+ window.gtag("set", "user_properties", traits);
111
159
  }
112
160
  return this.buildGtagResult();
113
161
  }
@@ -115,31 +163,60 @@ var GoogleAnalyticsProvider = class extends BaseProvider {
115
163
  if (typeof window === "undefined") {
116
164
  return this.buildGtagResult();
117
165
  }
118
- await this.ensureGtagLoaded();
119
- const config = {
166
+ await this.ensureGtag();
167
+ if (userId) {
168
+ window.gtag("set", { user_id: userId });
169
+ }
170
+ window.gtag("event", "page_view", {
120
171
  page_title: name,
121
172
  page_location: url,
122
- page_referrer: referrer,
123
- send_page_view: true
173
+ page_referrer: referrer
174
+ });
175
+ return this.buildGtagResult();
176
+ }
177
+ buildGtagResult() {
178
+ return {
179
+ provider: this.name,
180
+ success: true,
181
+ statusCode: 200
124
182
  };
125
- if (userId) {
126
- config.user_id = userId;
183
+ }
184
+ // ---------------------------------------------------------------------------
185
+ // Consent Mode v2
186
+ // ---------------------------------------------------------------------------
187
+ /**
188
+ * Updates the consent state at runtime. In browser mode this emits
189
+ * `gtag('consent', 'update', ...)`. Call this when the user interacts
190
+ * with a cookie/consent banner.
191
+ *
192
+ * To enable Google Signals demographics (age, gender, interests), grant
193
+ * at least `ad_personalization` and `ad_user_data`.
194
+ *
195
+ * In server mode this is a no-op — the Measurement Protocol does not
196
+ * support Consent Mode.
197
+ */
198
+ async updateConsent(consent) {
199
+ if (!this.isBrowser || typeof window === "undefined") {
200
+ return;
127
201
  }
128
- window.gtag("config", this.config.measurementId, config);
129
- return this.buildGtagResult();
202
+ await this.ensureGtag();
203
+ window.gtag("consent", "update", consent);
130
204
  }
205
+ // ---------------------------------------------------------------------------
206
+ // Public API — routes to browser or server mode
207
+ // ---------------------------------------------------------------------------
131
208
  async track({ event, properties, userId, anonymousId, timestamp }) {
132
209
  if (this.isBrowser) {
133
210
  return this.trackBrowser({ event, properties, userId, anonymousId, timestamp });
134
211
  }
135
212
  try {
136
- const client_id = anonymousId ?? this.config.clientId ?? "anonymous";
213
+ const clientId = anonymousId ?? this.config.clientId ?? "anonymous";
137
214
  const params = { ...properties };
138
215
  if (timestamp) {
139
216
  params["timestamp_micros"] = timestamp.getTime() * 1e3;
140
217
  }
141
218
  const body = {
142
- client_id,
219
+ client_id: clientId,
143
220
  events: [{ name: event, params }]
144
221
  };
145
222
  if (userId) {
@@ -685,6 +762,18 @@ var Mytart = class {
685
762
  };
686
763
  return Promise.all(this.providers.map((p) => p.page(enriched)));
687
764
  }
765
+ /**
766
+ * Update consent state across all providers that support consent management.
767
+ * Currently this is meaningful for Google Analytics (Consent Mode v2) in
768
+ * browser mode. Call this when the user interacts with a cookie/consent
769
+ * banner.
770
+ *
771
+ * To enable Google Signals demographics (age, gender, interests), grant
772
+ * at least `ad_personalization` and `ad_user_data`.
773
+ */
774
+ async updateConsent(consent) {
775
+ await Promise.all(this.providers.map((p) => p.updateConsent(consent)));
776
+ }
688
777
  addProvider(config) {
689
778
  if (config.enabled !== true) return;
690
779
  this.providers.push(createProvider(config));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mytart",
3
- "version": "0.2.4",
3
+ "version": "0.4.0",
4
4
  "description": "Multi-Yield Tracking & Analytics Relay Tool — framework-agnostic analytics for any project",
5
5
  "keywords": [
6
6
  "analytics",