mytart 0.2.3 → 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,73 +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}?id=${this.config.measurementId}`;
114
- script.onload = () => {
115
- window.gtag("js", /* @__PURE__ */ new Date());
116
- window.gtag("config", this.config.measurementId);
117
- resolve();
118
- };
128
+ script.src = `${GTAG_SCRIPT_URL}?id=${this.config.measurementId}`;
129
+ script.onload = () => resolve();
119
130
  script.onerror = () => resolve();
120
- const firstScript = document.getElementsByTagName("script")[0];
121
- firstScript.parentNode?.insertBefore(script, firstScript);
131
+ document.head.appendChild(script);
122
132
  });
123
133
  }
124
- buildGtagResult() {
125
- return {
126
- provider: this.name,
127
- success: true,
128
- statusCode: 200
129
- };
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;
130
143
  }
144
+ // ---------------------------------------------------------------------------
145
+ // Browser mode methods — delegate to gtag()
146
+ // ---------------------------------------------------------------------------
131
147
  async trackBrowser({ event, properties, userId }) {
132
148
  if (typeof window === "undefined") {
133
149
  return this.buildGtagResult();
134
150
  }
135
- await this.ensureGtagLoaded();
151
+ await this.ensureGtag();
136
152
  if (userId) {
137
153
  window.gtag("set", { user_id: userId });
138
154
  }
139
- window.gtag("event", event, properties || {});
155
+ window.gtag("event", event, properties ?? {});
140
156
  return this.buildGtagResult();
141
157
  }
142
158
  async identifyBrowser({ userId, traits }) {
143
159
  if (typeof window === "undefined") {
144
160
  return this.buildGtagResult();
145
161
  }
146
- await this.ensureGtagLoaded();
162
+ await this.ensureGtag();
147
163
  window.gtag("set", { user_id: userId });
148
164
  if (traits) {
149
- Object.entries(traits).forEach(([key, value]) => {
150
- window.gtag("set", { [key]: value });
151
- });
165
+ window.gtag("set", "user_properties", traits);
152
166
  }
153
167
  return this.buildGtagResult();
154
168
  }
@@ -156,31 +170,39 @@ var GoogleAnalyticsProvider = class extends BaseProvider {
156
170
  if (typeof window === "undefined") {
157
171
  return this.buildGtagResult();
158
172
  }
159
- await this.ensureGtagLoaded();
160
- const config = {
161
- page_title: name,
162
- page_location: url,
163
- page_referrer: referrer,
164
- send_page_view: true
165
- };
173
+ await this.ensureGtag();
166
174
  if (userId) {
167
- config.user_id = userId;
175
+ window.gtag("set", { user_id: userId });
168
176
  }
169
- 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
+ });
170
182
  return this.buildGtagResult();
171
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
+ // ---------------------------------------------------------------------------
172
194
  async track({ event, properties, userId, anonymousId, timestamp }) {
173
195
  if (this.isBrowser) {
174
196
  return this.trackBrowser({ event, properties, userId, anonymousId, timestamp });
175
197
  }
176
198
  try {
177
- const client_id = anonymousId ?? this.config.clientId ?? "anonymous";
199
+ const clientId = anonymousId ?? this.config.clientId ?? "anonymous";
178
200
  const params = { ...properties };
179
201
  if (timestamp) {
180
202
  params["timestamp_micros"] = timestamp.getTime() * 1e3;
181
203
  }
182
204
  const body = {
183
- client_id,
205
+ client_id: clientId,
184
206
  events: [{ name: event, params }]
185
207
  };
186
208
  if (userId) {
package/dist/index.mjs CHANGED
@@ -39,73 +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}?id=${this.config.measurementId}`;
71
- script.onload = () => {
72
- window.gtag("js", /* @__PURE__ */ new Date());
73
- window.gtag("config", this.config.measurementId);
74
- resolve();
75
- };
85
+ script.src = `${GTAG_SCRIPT_URL}?id=${this.config.measurementId}`;
86
+ script.onload = () => resolve();
76
87
  script.onerror = () => resolve();
77
- const firstScript = document.getElementsByTagName("script")[0];
78
- firstScript.parentNode?.insertBefore(script, firstScript);
88
+ document.head.appendChild(script);
79
89
  });
80
90
  }
81
- buildGtagResult() {
82
- return {
83
- provider: this.name,
84
- success: true,
85
- statusCode: 200
86
- };
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;
87
100
  }
101
+ // ---------------------------------------------------------------------------
102
+ // Browser mode methods — delegate to gtag()
103
+ // ---------------------------------------------------------------------------
88
104
  async trackBrowser({ event, properties, userId }) {
89
105
  if (typeof window === "undefined") {
90
106
  return this.buildGtagResult();
91
107
  }
92
- await this.ensureGtagLoaded();
108
+ await this.ensureGtag();
93
109
  if (userId) {
94
110
  window.gtag("set", { user_id: userId });
95
111
  }
96
- window.gtag("event", event, properties || {});
112
+ window.gtag("event", event, properties ?? {});
97
113
  return this.buildGtagResult();
98
114
  }
99
115
  async identifyBrowser({ userId, traits }) {
100
116
  if (typeof window === "undefined") {
101
117
  return this.buildGtagResult();
102
118
  }
103
- await this.ensureGtagLoaded();
119
+ await this.ensureGtag();
104
120
  window.gtag("set", { user_id: userId });
105
121
  if (traits) {
106
- Object.entries(traits).forEach(([key, value]) => {
107
- window.gtag("set", { [key]: value });
108
- });
122
+ window.gtag("set", "user_properties", traits);
109
123
  }
110
124
  return this.buildGtagResult();
111
125
  }
@@ -113,31 +127,39 @@ var GoogleAnalyticsProvider = class extends BaseProvider {
113
127
  if (typeof window === "undefined") {
114
128
  return this.buildGtagResult();
115
129
  }
116
- await this.ensureGtagLoaded();
117
- const config = {
118
- page_title: name,
119
- page_location: url,
120
- page_referrer: referrer,
121
- send_page_view: true
122
- };
130
+ await this.ensureGtag();
123
131
  if (userId) {
124
- config.user_id = userId;
132
+ window.gtag("set", { user_id: userId });
125
133
  }
126
- 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
+ });
127
139
  return this.buildGtagResult();
128
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
+ // ---------------------------------------------------------------------------
129
151
  async track({ event, properties, userId, anonymousId, timestamp }) {
130
152
  if (this.isBrowser) {
131
153
  return this.trackBrowser({ event, properties, userId, anonymousId, timestamp });
132
154
  }
133
155
  try {
134
- const client_id = anonymousId ?? this.config.clientId ?? "anonymous";
156
+ const clientId = anonymousId ?? this.config.clientId ?? "anonymous";
135
157
  const params = { ...properties };
136
158
  if (timestamp) {
137
159
  params["timestamp_micros"] = timestamp.getTime() * 1e3;
138
160
  }
139
161
  const body = {
140
- client_id,
162
+ client_id: clientId,
141
163
  events: [{ name: event, params }]
142
164
  };
143
165
  if (userId) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mytart",
3
- "version": "0.2.3",
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",