mytart 0.2.4 → 0.3.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,42 @@ 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
+
53
73
  ```typescript
54
- { provider: 'google-analytics', measurementId: 'G-XXXXXXXXXX', apiSecret: 'YOUR_SECRET', debug?: boolean }
74
+ {
75
+ provider: 'google-analytics',
76
+ measurementId: 'G-XXXXXXXXXX',
77
+ appType: 'browser',
78
+ enabled: true,
79
+ }
55
80
  ```
56
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
+
57
89
  ### Mixpanel
58
90
 
59
91
  ```typescript
@@ -187,7 +219,7 @@ All types are exported:
187
219
  ```typescript
188
220
  import type {
189
221
  MytartConfig, BaseProviderConfig, ProviderConfig, TrackOptions, IdentifyOptions, PageOptions,
190
- TrackResult, MytartError, EventContext, ProviderName,
222
+ TrackResult, MytartError, EventContext, ProviderName, GoogleAnalyticsAppType,
191
223
  GoogleAnalyticsConfig, MixpanelConfig, SegmentConfig,
192
224
  AmplitudeConfig, PlausibleConfig, PostHogConfig,
193
225
  } from 'mytart';
package/dist/index.d.mts CHANGED
@@ -126,34 +126,44 @@ declare global {
126
126
  dataLayer: unknown[];
127
127
  }
128
128
  }
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
- }
129
+ type GtagFn = (...args: unknown[]) => void;
143
130
  declare class GoogleAnalyticsProvider extends BaseProvider {
144
131
  readonly name = "google-analytics";
145
132
  private readonly config;
146
133
  private readonly http;
147
134
  private readonly endpoint;
148
135
  private readonly isBrowser;
149
- private gtagInitialized;
136
+ private gtagReady;
150
137
  constructor(config: GoogleAnalyticsConfig);
151
- private ensureGtagLoaded;
152
- private injectGtagScript;
153
- private buildGtagResult;
138
+ /**
139
+ * Initializes the gtag.js snippet exactly as Google's official documentation
140
+ * specifies. This mirrors the standard snippet:
141
+ *
142
+ * <script async src="https://www.googletagmanager.com/gtag/js?id=TAG_ID"></script>
143
+ * <script>
144
+ * window.dataLayer = window.dataLayer || [];
145
+ * function gtag(){dataLayer.push(arguments);}
146
+ * gtag('js', new Date());
147
+ * gtag('config', 'TAG_ID');
148
+ * </script>
149
+ *
150
+ * Key details:
151
+ * - The script URL MUST include ?id=TAG_ID for Google Tag Tester detection
152
+ * - dataLayer and the gtag shim are set up BEFORE the script loads
153
+ * - gtag('js') and gtag('config') are called synchronously — they queue
154
+ * into dataLayer and are processed once the real script loads
155
+ * - The returned promise resolves when the script finishes loading
156
+ */
157
+ private initGtag;
158
+ /**
159
+ * Ensures gtag is initialized exactly once. Subsequent calls return the
160
+ * same promise so the script is never injected twice.
161
+ */
162
+ private ensureGtag;
154
163
  private trackBrowser;
155
164
  private identifyBrowser;
156
165
  private pageBrowser;
166
+ private buildGtagResult;
157
167
  track({ event, properties, userId, anonymousId, timestamp }: TrackOptions): Promise<TrackResult>;
158
168
  identify({ userId, traits }: IdentifyOptions): Promise<TrackResult>;
159
169
  page({ name, url, referrer, userId, anonymousId }: PageOptions): Promise<TrackResult>;
package/dist/index.d.ts CHANGED
@@ -126,34 +126,44 @@ declare global {
126
126
  dataLayer: unknown[];
127
127
  }
128
128
  }
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
- }
129
+ type GtagFn = (...args: unknown[]) => void;
143
130
  declare class GoogleAnalyticsProvider extends BaseProvider {
144
131
  readonly name = "google-analytics";
145
132
  private readonly config;
146
133
  private readonly http;
147
134
  private readonly endpoint;
148
135
  private readonly isBrowser;
149
- private gtagInitialized;
136
+ private gtagReady;
150
137
  constructor(config: GoogleAnalyticsConfig);
151
- private ensureGtagLoaded;
152
- private injectGtagScript;
153
- private buildGtagResult;
138
+ /**
139
+ * Initializes the gtag.js snippet exactly as Google's official documentation
140
+ * specifies. This mirrors the standard snippet:
141
+ *
142
+ * <script async src="https://www.googletagmanager.com/gtag/js?id=TAG_ID"></script>
143
+ * <script>
144
+ * window.dataLayer = window.dataLayer || [];
145
+ * function gtag(){dataLayer.push(arguments);}
146
+ * gtag('js', new Date());
147
+ * gtag('config', 'TAG_ID');
148
+ * </script>
149
+ *
150
+ * Key details:
151
+ * - The script URL MUST include ?id=TAG_ID for Google Tag Tester detection
152
+ * - dataLayer and the gtag shim are set up BEFORE the script loads
153
+ * - gtag('js') and gtag('config') are called synchronously — they queue
154
+ * into dataLayer and are processed once the real script loads
155
+ * - The returned promise resolves when the script finishes loading
156
+ */
157
+ private initGtag;
158
+ /**
159
+ * Ensures gtag is initialized exactly once. Subsequent calls return the
160
+ * same promise so the script is never injected twice.
161
+ */
162
+ private ensureGtag;
154
163
  private trackBrowser;
155
164
  private identifyBrowser;
156
165
  private pageBrowser;
166
+ private buildGtagResult;
157
167
  track({ event, properties, userId, anonymousId, timestamp }: TrackOptions): Promise<TrackResult>;
158
168
  identify({ userId, traits }: IdentifyOptions): Promise<TrackResult>;
159
169
  page({ name, url, referrer, userId, anonymousId }: PageOptions): Promise<TrackResult>;
package/dist/index.js CHANGED
@@ -82,75 +82,87 @@ function isAxiosError(error) {
82
82
  // src/providers/google-analytics.ts
83
83
  var GA4_ENDPOINT = "https://www.google-analytics.com/mp/collect";
84
84
  var GA4_DEBUG_ENDPOINT = "https://www.google-analytics.com/debug/mp/collect";
85
- var GTAG_URL = "https://www.googletagmanager.com/gtag/js";
85
+ var GTAG_SCRIPT_URL = "https://www.googletagmanager.com/gtag/js";
86
86
  var GoogleAnalyticsProvider = class extends BaseProvider {
87
87
  constructor(config) {
88
88
  super();
89
89
  this.name = "google-analytics";
90
- this.gtagInitialized = false;
90
+ this.gtagReady = null;
91
91
  this.config = config;
92
92
  this.http = createHttpClient();
93
93
  this.endpoint = config.debug ? GA4_DEBUG_ENDPOINT : GA4_ENDPOINT;
94
94
  this.isBrowser = config.appType === "browser";
95
95
  }
96
- async ensureGtagLoaded() {
97
- if (typeof window === "undefined") {
98
- return;
99
- }
100
- if (typeof window.gtag === "function") {
101
- return;
96
+ /**
97
+ * Initializes the gtag.js snippet exactly as Google's official documentation
98
+ * specifies. This mirrors the standard snippet:
99
+ *
100
+ * <script async src="https://www.googletagmanager.com/gtag/js?id=TAG_ID"></script>
101
+ * <script>
102
+ * window.dataLayer = window.dataLayer || [];
103
+ * function gtag(){dataLayer.push(arguments);}
104
+ * gtag('js', new Date());
105
+ * gtag('config', 'TAG_ID');
106
+ * </script>
107
+ *
108
+ * Key details:
109
+ * - The script URL MUST include ?id=TAG_ID for Google Tag Tester detection
110
+ * - dataLayer and the gtag shim are set up BEFORE the script loads
111
+ * - gtag('js') and gtag('config') are called synchronously — they queue
112
+ * into dataLayer and are processed once the real script loads
113
+ * - The returned promise resolves when the script finishes loading
114
+ */
115
+ initGtag() {
116
+ if (typeof window === "undefined" || typeof document === "undefined") {
117
+ return Promise.resolve();
102
118
  }
103
- await this.injectGtagScript();
104
- }
105
- injectGtagScript() {
119
+ window.dataLayer = window.dataLayer || [];
120
+ window.gtag = function gtag() {
121
+ window.dataLayer.push(arguments);
122
+ };
123
+ window.gtag("js", /* @__PURE__ */ new Date());
124
+ window.gtag("config", this.config.measurementId);
106
125
  return new Promise((resolve) => {
107
- window.dataLayer = window.dataLayer || [];
108
- window.gtag = window.gtag || function gtag(...args) {
109
- window.dataLayer.push(args);
110
- };
111
126
  const script = document.createElement("script");
112
127
  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
- };
128
+ script.src = `${GTAG_SCRIPT_URL}?id=${this.config.measurementId}`;
129
+ script.onload = () => resolve();
121
130
  script.onerror = () => resolve();
122
- const firstScript = document.getElementsByTagName("script")[0];
123
- firstScript.parentNode?.insertBefore(script, firstScript);
131
+ document.head.appendChild(script);
124
132
  });
125
133
  }
126
- buildGtagResult() {
127
- return {
128
- provider: this.name,
129
- success: true,
130
- statusCode: 200
131
- };
134
+ /**
135
+ * Ensures gtag is initialized exactly once. Subsequent calls return the
136
+ * same promise so the script is never injected twice.
137
+ */
138
+ ensureGtag() {
139
+ if (!this.gtagReady) {
140
+ this.gtagReady = this.initGtag();
141
+ }
142
+ return this.gtagReady;
132
143
  }
144
+ // ---------------------------------------------------------------------------
145
+ // Browser mode methods — delegate to gtag()
146
+ // ---------------------------------------------------------------------------
133
147
  async trackBrowser({ event, properties, userId }) {
134
148
  if (typeof window === "undefined") {
135
149
  return this.buildGtagResult();
136
150
  }
137
- await this.ensureGtagLoaded();
151
+ await this.ensureGtag();
138
152
  if (userId) {
139
153
  window.gtag("set", { user_id: userId });
140
154
  }
141
- window.gtag("event", event, properties || {});
155
+ window.gtag("event", event, properties ?? {});
142
156
  return this.buildGtagResult();
143
157
  }
144
158
  async identifyBrowser({ userId, traits }) {
145
159
  if (typeof window === "undefined") {
146
160
  return this.buildGtagResult();
147
161
  }
148
- await this.ensureGtagLoaded();
162
+ await this.ensureGtag();
149
163
  window.gtag("set", { user_id: userId });
150
164
  if (traits) {
151
- Object.entries(traits).forEach(([key, value]) => {
152
- window.gtag("set", { [key]: value });
153
- });
165
+ window.gtag("set", "user_properties", traits);
154
166
  }
155
167
  return this.buildGtagResult();
156
168
  }
@@ -158,31 +170,39 @@ var GoogleAnalyticsProvider = class extends BaseProvider {
158
170
  if (typeof window === "undefined") {
159
171
  return this.buildGtagResult();
160
172
  }
161
- await this.ensureGtagLoaded();
162
- const config = {
163
- page_title: name,
164
- page_location: url,
165
- page_referrer: referrer,
166
- send_page_view: true
167
- };
173
+ await this.ensureGtag();
168
174
  if (userId) {
169
- config.user_id = userId;
175
+ window.gtag("set", { user_id: userId });
170
176
  }
171
- window.gtag("config", this.config.measurementId, config);
177
+ window.gtag("event", "page_view", {
178
+ page_title: name,
179
+ page_location: url,
180
+ page_referrer: referrer
181
+ });
172
182
  return this.buildGtagResult();
173
183
  }
184
+ buildGtagResult() {
185
+ return {
186
+ provider: this.name,
187
+ success: true,
188
+ statusCode: 200
189
+ };
190
+ }
191
+ // ---------------------------------------------------------------------------
192
+ // Public API — routes to browser or server mode
193
+ // ---------------------------------------------------------------------------
174
194
  async track({ event, properties, userId, anonymousId, timestamp }) {
175
195
  if (this.isBrowser) {
176
196
  return this.trackBrowser({ event, properties, userId, anonymousId, timestamp });
177
197
  }
178
198
  try {
179
- const client_id = anonymousId ?? this.config.clientId ?? "anonymous";
199
+ const clientId = anonymousId ?? this.config.clientId ?? "anonymous";
180
200
  const params = { ...properties };
181
201
  if (timestamp) {
182
202
  params["timestamp_micros"] = timestamp.getTime() * 1e3;
183
203
  }
184
204
  const body = {
185
- client_id,
205
+ client_id: clientId,
186
206
  events: [{ name: event, params }]
187
207
  };
188
208
  if (userId) {
package/dist/index.mjs CHANGED
@@ -39,75 +39,87 @@ function isAxiosError(error) {
39
39
  // src/providers/google-analytics.ts
40
40
  var GA4_ENDPOINT = "https://www.google-analytics.com/mp/collect";
41
41
  var GA4_DEBUG_ENDPOINT = "https://www.google-analytics.com/debug/mp/collect";
42
- var GTAG_URL = "https://www.googletagmanager.com/gtag/js";
42
+ var GTAG_SCRIPT_URL = "https://www.googletagmanager.com/gtag/js";
43
43
  var GoogleAnalyticsProvider = class extends BaseProvider {
44
44
  constructor(config) {
45
45
  super();
46
46
  this.name = "google-analytics";
47
- this.gtagInitialized = false;
47
+ this.gtagReady = null;
48
48
  this.config = config;
49
49
  this.http = createHttpClient();
50
50
  this.endpoint = config.debug ? GA4_DEBUG_ENDPOINT : GA4_ENDPOINT;
51
51
  this.isBrowser = config.appType === "browser";
52
52
  }
53
- async ensureGtagLoaded() {
54
- if (typeof window === "undefined") {
55
- return;
56
- }
57
- if (typeof window.gtag === "function") {
58
- return;
53
+ /**
54
+ * Initializes the gtag.js snippet exactly as Google's official documentation
55
+ * specifies. This mirrors the standard snippet:
56
+ *
57
+ * <script async src="https://www.googletagmanager.com/gtag/js?id=TAG_ID"></script>
58
+ * <script>
59
+ * window.dataLayer = window.dataLayer || [];
60
+ * function gtag(){dataLayer.push(arguments);}
61
+ * gtag('js', new Date());
62
+ * gtag('config', 'TAG_ID');
63
+ * </script>
64
+ *
65
+ * Key details:
66
+ * - The script URL MUST include ?id=TAG_ID for Google Tag Tester detection
67
+ * - dataLayer and the gtag shim are set up BEFORE the script loads
68
+ * - gtag('js') and gtag('config') are called synchronously — they queue
69
+ * into dataLayer and are processed once the real script loads
70
+ * - The returned promise resolves when the script finishes loading
71
+ */
72
+ initGtag() {
73
+ if (typeof window === "undefined" || typeof document === "undefined") {
74
+ return Promise.resolve();
59
75
  }
60
- await this.injectGtagScript();
61
- }
62
- injectGtagScript() {
76
+ window.dataLayer = window.dataLayer || [];
77
+ window.gtag = function gtag() {
78
+ window.dataLayer.push(arguments);
79
+ };
80
+ window.gtag("js", /* @__PURE__ */ new Date());
81
+ window.gtag("config", this.config.measurementId);
63
82
  return new Promise((resolve) => {
64
- window.dataLayer = window.dataLayer || [];
65
- window.gtag = window.gtag || function gtag(...args) {
66
- window.dataLayer.push(args);
67
- };
68
83
  const script = document.createElement("script");
69
84
  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
- };
85
+ script.src = `${GTAG_SCRIPT_URL}?id=${this.config.measurementId}`;
86
+ script.onload = () => resolve();
78
87
  script.onerror = () => resolve();
79
- const firstScript = document.getElementsByTagName("script")[0];
80
- firstScript.parentNode?.insertBefore(script, firstScript);
88
+ document.head.appendChild(script);
81
89
  });
82
90
  }
83
- buildGtagResult() {
84
- return {
85
- provider: this.name,
86
- success: true,
87
- statusCode: 200
88
- };
91
+ /**
92
+ * Ensures gtag is initialized exactly once. Subsequent calls return the
93
+ * same promise so the script is never injected twice.
94
+ */
95
+ ensureGtag() {
96
+ if (!this.gtagReady) {
97
+ this.gtagReady = this.initGtag();
98
+ }
99
+ return this.gtagReady;
89
100
  }
101
+ // ---------------------------------------------------------------------------
102
+ // Browser mode methods — delegate to gtag()
103
+ // ---------------------------------------------------------------------------
90
104
  async trackBrowser({ event, properties, userId }) {
91
105
  if (typeof window === "undefined") {
92
106
  return this.buildGtagResult();
93
107
  }
94
- await this.ensureGtagLoaded();
108
+ await this.ensureGtag();
95
109
  if (userId) {
96
110
  window.gtag("set", { user_id: userId });
97
111
  }
98
- window.gtag("event", event, properties || {});
112
+ window.gtag("event", event, properties ?? {});
99
113
  return this.buildGtagResult();
100
114
  }
101
115
  async identifyBrowser({ userId, traits }) {
102
116
  if (typeof window === "undefined") {
103
117
  return this.buildGtagResult();
104
118
  }
105
- await this.ensureGtagLoaded();
119
+ await this.ensureGtag();
106
120
  window.gtag("set", { user_id: userId });
107
121
  if (traits) {
108
- Object.entries(traits).forEach(([key, value]) => {
109
- window.gtag("set", { [key]: value });
110
- });
122
+ window.gtag("set", "user_properties", traits);
111
123
  }
112
124
  return this.buildGtagResult();
113
125
  }
@@ -115,31 +127,39 @@ var GoogleAnalyticsProvider = class extends BaseProvider {
115
127
  if (typeof window === "undefined") {
116
128
  return this.buildGtagResult();
117
129
  }
118
- await this.ensureGtagLoaded();
119
- const config = {
120
- page_title: name,
121
- page_location: url,
122
- page_referrer: referrer,
123
- send_page_view: true
124
- };
130
+ await this.ensureGtag();
125
131
  if (userId) {
126
- config.user_id = userId;
132
+ window.gtag("set", { user_id: userId });
127
133
  }
128
- window.gtag("config", this.config.measurementId, config);
134
+ window.gtag("event", "page_view", {
135
+ page_title: name,
136
+ page_location: url,
137
+ page_referrer: referrer
138
+ });
129
139
  return this.buildGtagResult();
130
140
  }
141
+ buildGtagResult() {
142
+ return {
143
+ provider: this.name,
144
+ success: true,
145
+ statusCode: 200
146
+ };
147
+ }
148
+ // ---------------------------------------------------------------------------
149
+ // Public API — routes to browser or server mode
150
+ // ---------------------------------------------------------------------------
131
151
  async track({ event, properties, userId, anonymousId, timestamp }) {
132
152
  if (this.isBrowser) {
133
153
  return this.trackBrowser({ event, properties, userId, anonymousId, timestamp });
134
154
  }
135
155
  try {
136
- const client_id = anonymousId ?? this.config.clientId ?? "anonymous";
156
+ const clientId = anonymousId ?? this.config.clientId ?? "anonymous";
137
157
  const params = { ...properties };
138
158
  if (timestamp) {
139
159
  params["timestamp_micros"] = timestamp.getTime() * 1e3;
140
160
  }
141
161
  const body = {
142
- client_id,
162
+ client_id: clientId,
143
163
  events: [{ name: event, params }]
144
164
  };
145
165
  if (userId) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mytart",
3
- "version": "0.2.4",
3
+ "version": "0.3.0",
4
4
  "description": "Multi-Yield Tracking & Analytics Relay Tool — framework-agnostic analytics for any project",
5
5
  "keywords": [
6
6
  "analytics",