@happyvertical/analytics 0.79.0 → 0.80.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.
package/AGENT.md CHANGED
@@ -7,7 +7,7 @@ Unified analytics interface for Google Analytics 4, Plausible, and more
7
7
  ## Package Map
8
8
  - Package: `@happyvertical/analytics`
9
9
  - Hierarchy path: `@happyvertical/sdk > packages > analytics`
10
- - Workspace position: `3 of 31` local packages
10
+ - Workspace position: `3 of 32` local packages
11
11
  - Internal dependencies: `@happyvertical/utils`
12
12
  - Internal dependents: none
13
13
  - Knowledge graph files: `AGENT.md`, `metadata.json`, `ecosystem-manifest.json`
@@ -0,0 +1,668 @@
1
+ import { c as RateLimitError, n as AuthenticationError, o as PropertyNotFoundError, s as QuotaExceededError, t as AnalyticsError } from "./types-zsbKROGX.js";
2
+ import { readFile } from "node:fs/promises";
3
+ import { google } from "googleapis";
4
+ //#region src/shared/providers/ga4.ts
5
+ /**
6
+ * Google Analytics 4 provider implementation
7
+ *
8
+ * Uses three APIs internally:
9
+ * - Admin API: Property management, data streams, custom definitions
10
+ * - Data API: Reports and analytics queries
11
+ * - Measurement Protocol: Server-side event tracking
12
+ */
13
+ /**
14
+ * Measurement Protocol endpoint
15
+ */
16
+ var MEASUREMENT_PROTOCOL_URL = "https://www.google-analytics.com/mp/collect";
17
+ var PROPERTY_HYDRATION_CONCURRENCY = 5;
18
+ /**
19
+ * Google Analytics 4 provider
20
+ */
21
+ var GA4Provider = class {
22
+ adminClient = null;
23
+ dataClient = null;
24
+ options;
25
+ credentials = null;
26
+ constructor(options) {
27
+ this.options = {
28
+ timeout: 3e4,
29
+ maxRetries: 3,
30
+ cacheTTL: 36e5,
31
+ ...options
32
+ };
33
+ }
34
+ /**
35
+ * Initialize Google API clients with service account credentials
36
+ */
37
+ async ensureClients() {
38
+ if (this.adminClient && this.dataClient) return;
39
+ if (!this.credentials && this.options.serviceAccountKey) if (typeof this.options.serviceAccountKey === "string") {
40
+ const content = await readFile(this.options.serviceAccountKey, "utf-8");
41
+ this.credentials = JSON.parse(content);
42
+ } else this.credentials = this.options.serviceAccountKey;
43
+ if (!this.credentials) throw new AuthenticationError("ga4");
44
+ const auth = new google.auth.GoogleAuth({
45
+ credentials: this.credentials,
46
+ scopes: ["https://www.googleapis.com/auth/analytics.readonly", "https://www.googleapis.com/auth/analytics.edit"]
47
+ });
48
+ this.adminClient = google.analyticsadmin({
49
+ version: "v1beta",
50
+ auth
51
+ });
52
+ this.dataClient = google.analyticsdata({
53
+ version: "v1beta",
54
+ auth
55
+ });
56
+ }
57
+ /**
58
+ * Normalize property ID to numeric format
59
+ */
60
+ normalizePropertyId(propertyId) {
61
+ return propertyId.replace(/^properties\//, "");
62
+ }
63
+ /**
64
+ * Get full property resource name
65
+ */
66
+ getPropertyName(propertyId) {
67
+ return `properties/${this.normalizePropertyId(propertyId)}`;
68
+ }
69
+ /**
70
+ * Map a Google Analytics property resource to the public Property shape
71
+ */
72
+ mapProperty(property) {
73
+ return {
74
+ id: property.name?.replace("properties/", "") || "",
75
+ name: property.name || "",
76
+ displayName: property.displayName || "",
77
+ createTime: property.createTime || "",
78
+ updateTime: property.updateTime ?? void 0,
79
+ timeZone: property.timeZone ?? void 0,
80
+ currencyCode: property.currencyCode ?? void 0,
81
+ industryCategory: property.industryCategory ?? void 0,
82
+ serviceLevel: property.serviceLevel
83
+ };
84
+ }
85
+ /**
86
+ * Hydrate discovered properties without firing unbounded concurrent requests
87
+ */
88
+ async hydrateProperties(properties) {
89
+ const hydrated = [];
90
+ for (let index = 0; index < properties.length; index += PROPERTY_HYDRATION_CONCURRENCY) {
91
+ const batch = properties.slice(index, index + PROPERTY_HYDRATION_CONCURRENCY);
92
+ const hydratedBatch = await Promise.all(batch.map(async (property) => {
93
+ const response = await this.adminClient.properties.get({ name: property.name });
94
+ return this.mapProperty(response.data);
95
+ }));
96
+ hydrated.push(...hydratedBatch);
97
+ }
98
+ return hydrated;
99
+ }
100
+ /**
101
+ * Map Google API errors to our error types
102
+ */
103
+ mapError(error) {
104
+ if (error instanceof AnalyticsError) return error;
105
+ const err = error;
106
+ const status = err.code || err.status;
107
+ const message = err.message || "Unknown error";
108
+ switch (status) {
109
+ case 401:
110
+ case 403: return new AuthenticationError("ga4");
111
+ case 404: return new PropertyNotFoundError(message, "ga4");
112
+ case 429: return new RateLimitError("ga4");
113
+ case 402: return new QuotaExceededError("ga4");
114
+ default: return new AnalyticsError(message, "API_ERROR", "ga4");
115
+ }
116
+ }
117
+ async createProperty(options) {
118
+ await this.ensureClients();
119
+ if (!options.parent) throw new AnalyticsError("Parent account is required for creating GA4 properties", "MISSING_PARENT", "ga4");
120
+ try {
121
+ const response = await this.adminClient.properties.create({ requestBody: {
122
+ displayName: options.displayName,
123
+ timeZone: options.timeZone || "America/Los_Angeles",
124
+ currencyCode: options.currencyCode || "USD",
125
+ industryCategory: options.industryCategory,
126
+ parent: options.parent
127
+ } });
128
+ const property = this.mapProperty(response.data);
129
+ if (!property.createTime) property.createTime = (/* @__PURE__ */ new Date()).toISOString();
130
+ return property;
131
+ } catch (error) {
132
+ throw this.mapError(error);
133
+ }
134
+ }
135
+ async listProperties(options) {
136
+ await this.ensureClients();
137
+ try {
138
+ const properties = /* @__PURE__ */ new Map();
139
+ let pageToken;
140
+ do {
141
+ const response = await this.adminClient.accountSummaries.list({
142
+ pageSize: 200,
143
+ pageToken
144
+ });
145
+ for (const accountSummary of response.data.accountSummaries || []) for (const propertySummary of accountSummary.propertySummaries || []) {
146
+ const propertyName = propertySummary.property || "";
147
+ if (!propertyName || properties.has(propertyName)) continue;
148
+ properties.set(propertyName, {
149
+ id: propertyName.replace("properties/", ""),
150
+ name: propertyName,
151
+ displayName: propertySummary.displayName || "",
152
+ createTime: ""
153
+ });
154
+ }
155
+ pageToken = response.data.nextPageToken ?? void 0;
156
+ } while (pageToken);
157
+ const listedProperties = [...properties.values()];
158
+ if (options?.hydrate === false) return listedProperties;
159
+ return this.hydrateProperties(listedProperties);
160
+ } catch (error) {
161
+ throw this.mapError(error);
162
+ }
163
+ }
164
+ async getProperty(propertyId) {
165
+ await this.ensureClients();
166
+ try {
167
+ const response = await this.adminClient.properties.get({ name: this.getPropertyName(propertyId) });
168
+ return this.mapProperty(response.data);
169
+ } catch (error) {
170
+ throw this.mapError(error);
171
+ }
172
+ }
173
+ async updateProperty(propertyId, data) {
174
+ await this.ensureClients();
175
+ const updateMask = [];
176
+ if (data.displayName) updateMask.push("displayName");
177
+ if (data.timeZone) updateMask.push("timeZone");
178
+ if (data.currencyCode) updateMask.push("currencyCode");
179
+ if (data.industryCategory) updateMask.push("industryCategory");
180
+ try {
181
+ const response = await this.adminClient.properties.patch({
182
+ name: this.getPropertyName(propertyId),
183
+ updateMask: updateMask.join(","),
184
+ requestBody: {
185
+ displayName: data.displayName,
186
+ timeZone: data.timeZone,
187
+ currencyCode: data.currencyCode,
188
+ industryCategory: data.industryCategory
189
+ }
190
+ });
191
+ return this.mapProperty(response.data);
192
+ } catch (error) {
193
+ throw this.mapError(error);
194
+ }
195
+ }
196
+ async deleteProperty(propertyId) {
197
+ await this.ensureClients();
198
+ try {
199
+ await this.adminClient.properties.delete({ name: this.getPropertyName(propertyId) });
200
+ } catch (error) {
201
+ throw this.mapError(error);
202
+ }
203
+ }
204
+ async getDataStreams(propertyId) {
205
+ await this.ensureClients();
206
+ try {
207
+ return ((await this.adminClient.properties.dataStreams.list({ parent: this.getPropertyName(propertyId) })).data.dataStreams || []).map((ds) => ({
208
+ id: ds.name?.split("/").pop() || "",
209
+ type: ds.type,
210
+ displayName: ds.displayName || "",
211
+ measurementId: ds.webStreamData?.measurementId ?? void 0,
212
+ firebaseAppId: ds.androidAppStreamData?.firebaseAppId ?? ds.iosAppStreamData?.firebaseAppId ?? void 0,
213
+ defaultUri: ds.webStreamData?.defaultUri ?? void 0,
214
+ createTime: ds.createTime || "",
215
+ updateTime: ds.updateTime ?? void 0
216
+ }));
217
+ } catch (error) {
218
+ throw this.mapError(error);
219
+ }
220
+ }
221
+ async createDataStream(propertyId, options) {
222
+ await this.ensureClients();
223
+ try {
224
+ const requestBody = {
225
+ type: options.type,
226
+ displayName: options.displayName
227
+ };
228
+ if (options.type === "WEB_DATA_STREAM") requestBody.webStreamData = { defaultUri: options.defaultUri };
229
+ else if (options.type === "ANDROID_APP_DATA_STREAM") requestBody.androidAppStreamData = { packageName: options.packageName };
230
+ else if (options.type === "IOS_APP_DATA_STREAM") requestBody.iosAppStreamData = { bundleId: options.bundleId };
231
+ const ds = (await this.adminClient.properties.dataStreams.create({
232
+ parent: this.getPropertyName(propertyId),
233
+ requestBody
234
+ })).data;
235
+ return {
236
+ id: ds.name?.split("/").pop() || "",
237
+ type: ds.type,
238
+ displayName: ds.displayName || "",
239
+ measurementId: ds.webStreamData?.measurementId ?? void 0,
240
+ firebaseAppId: ds.androidAppStreamData?.firebaseAppId ?? ds.iosAppStreamData?.firebaseAppId ?? void 0,
241
+ defaultUri: ds.webStreamData?.defaultUri ?? void 0,
242
+ createTime: ds.createTime || "",
243
+ updateTime: ds.updateTime ?? void 0
244
+ };
245
+ } catch (error) {
246
+ throw this.mapError(error);
247
+ }
248
+ }
249
+ async deleteDataStream(propertyId, streamId) {
250
+ await this.ensureClients();
251
+ try {
252
+ await this.adminClient.properties.dataStreams.delete({ name: `${this.getPropertyName(propertyId)}/dataStreams/${streamId}` });
253
+ } catch (error) {
254
+ throw this.mapError(error);
255
+ }
256
+ }
257
+ async getCustomDimensions(propertyId) {
258
+ await this.ensureClients();
259
+ try {
260
+ return ((await this.adminClient.properties.customDimensions.list({ parent: this.getPropertyName(propertyId) })).data.customDimensions || []).map((cd) => ({
261
+ id: cd.name?.split("/").pop() || "",
262
+ name: cd.name || "",
263
+ parameterName: cd.parameterName || "",
264
+ displayName: cd.displayName || "",
265
+ description: cd.description ?? void 0,
266
+ scope: cd.scope,
267
+ disallowAdsPersonalization: cd.disallowAdsPersonalization ?? void 0
268
+ }));
269
+ } catch (error) {
270
+ throw this.mapError(error);
271
+ }
272
+ }
273
+ async createCustomDimension(propertyId, options) {
274
+ await this.ensureClients();
275
+ try {
276
+ const cd = (await this.adminClient.properties.customDimensions.create({
277
+ parent: this.getPropertyName(propertyId),
278
+ requestBody: {
279
+ parameterName: options.parameterName,
280
+ displayName: options.displayName,
281
+ description: options.description,
282
+ scope: options.scope,
283
+ disallowAdsPersonalization: options.disallowAdsPersonalization
284
+ }
285
+ })).data;
286
+ return {
287
+ id: cd.name?.split("/").pop() || "",
288
+ name: cd.name || "",
289
+ parameterName: cd.parameterName || "",
290
+ displayName: cd.displayName || "",
291
+ description: cd.description ?? void 0,
292
+ scope: cd.scope,
293
+ disallowAdsPersonalization: cd.disallowAdsPersonalization ?? void 0
294
+ };
295
+ } catch (error) {
296
+ throw this.mapError(error);
297
+ }
298
+ }
299
+ async archiveCustomDimension(propertyId, dimensionId) {
300
+ await this.ensureClients();
301
+ try {
302
+ await this.adminClient.properties.customDimensions.archive({ name: `${this.getPropertyName(propertyId)}/customDimensions/${dimensionId}` });
303
+ } catch (error) {
304
+ throw this.mapError(error);
305
+ }
306
+ }
307
+ async getCustomMetrics(propertyId) {
308
+ await this.ensureClients();
309
+ try {
310
+ return ((await this.adminClient.properties.customMetrics.list({ parent: this.getPropertyName(propertyId) })).data.customMetrics || []).map((cm) => ({
311
+ id: cm.name?.split("/").pop() || "",
312
+ name: cm.name || "",
313
+ parameterName: cm.parameterName || "",
314
+ displayName: cm.displayName || "",
315
+ description: cm.description ?? void 0,
316
+ scope: "EVENT",
317
+ measurementUnit: cm.measurementUnit,
318
+ restrictedMetricType: cm.restrictedMetricType?.[0] ?? void 0
319
+ }));
320
+ } catch (error) {
321
+ throw this.mapError(error);
322
+ }
323
+ }
324
+ async createCustomMetric(propertyId, options) {
325
+ await this.ensureClients();
326
+ try {
327
+ const cm = (await this.adminClient.properties.customMetrics.create({
328
+ parent: this.getPropertyName(propertyId),
329
+ requestBody: {
330
+ parameterName: options.parameterName,
331
+ displayName: options.displayName,
332
+ description: options.description,
333
+ measurementUnit: options.measurementUnit,
334
+ restrictedMetricType: options.restrictedMetricType ? [options.restrictedMetricType] : void 0,
335
+ scope: "EVENT"
336
+ }
337
+ })).data;
338
+ return {
339
+ id: cm.name?.split("/").pop() || "",
340
+ name: cm.name || "",
341
+ parameterName: cm.parameterName || "",
342
+ displayName: cm.displayName || "",
343
+ description: cm.description ?? void 0,
344
+ scope: "EVENT",
345
+ measurementUnit: cm.measurementUnit,
346
+ restrictedMetricType: cm.restrictedMetricType?.[0] ?? void 0
347
+ };
348
+ } catch (error) {
349
+ throw this.mapError(error);
350
+ }
351
+ }
352
+ async archiveCustomMetric(propertyId, metricId) {
353
+ await this.ensureClients();
354
+ try {
355
+ await this.adminClient.properties.customMetrics.archive({ name: `${this.getPropertyName(propertyId)}/customMetrics/${metricId}` });
356
+ } catch (error) {
357
+ throw this.mapError(error);
358
+ }
359
+ }
360
+ async getKeyEvents(propertyId) {
361
+ await this.ensureClients();
362
+ try {
363
+ return ((await this.adminClient.properties.keyEvents.list({ parent: this.getPropertyName(propertyId) })).data.keyEvents || []).map((ke) => ({
364
+ id: ke.name?.split("/").pop() || "",
365
+ name: ke.name || "",
366
+ eventName: ke.eventName || "",
367
+ createTime: ke.createTime || "",
368
+ countingMethod: ke.countingMethod,
369
+ defaultValue: ke.defaultValue ? {
370
+ numericValue: ke.defaultValue.numericValue ?? void 0,
371
+ currencyCode: ke.defaultValue.currencyCode ?? void 0
372
+ } : void 0
373
+ }));
374
+ } catch (error) {
375
+ throw this.mapError(error);
376
+ }
377
+ }
378
+ async createKeyEvent(propertyId, options) {
379
+ await this.ensureClients();
380
+ try {
381
+ const ke = (await this.adminClient.properties.keyEvents.create({
382
+ parent: this.getPropertyName(propertyId),
383
+ requestBody: {
384
+ eventName: options.eventName,
385
+ countingMethod: options.countingMethod,
386
+ defaultValue: options.defaultValue
387
+ }
388
+ })).data;
389
+ return {
390
+ id: ke.name?.split("/").pop() || "",
391
+ name: ke.name || "",
392
+ eventName: ke.eventName || "",
393
+ createTime: ke.createTime || "",
394
+ countingMethod: ke.countingMethod,
395
+ defaultValue: ke.defaultValue ? {
396
+ numericValue: ke.defaultValue.numericValue ?? void 0,
397
+ currencyCode: ke.defaultValue.currencyCode ?? void 0
398
+ } : void 0
399
+ };
400
+ } catch (error) {
401
+ throw this.mapError(error);
402
+ }
403
+ }
404
+ async deleteKeyEvent(propertyId, eventId) {
405
+ await this.ensureClients();
406
+ try {
407
+ await this.adminClient.properties.keyEvents.delete({ name: `${this.getPropertyName(propertyId)}/keyEvents/${eventId}` });
408
+ } catch (error) {
409
+ throw this.mapError(error);
410
+ }
411
+ }
412
+ async runReport(propertyId, options) {
413
+ await this.ensureClients();
414
+ try {
415
+ const data = (await this.dataClient.properties.runReport({
416
+ property: this.getPropertyName(propertyId),
417
+ requestBody: {
418
+ dateRanges: options.dateRanges,
419
+ dimensions: options.dimensions,
420
+ metrics: options.metrics,
421
+ dimensionFilter: options.dimensionFilter,
422
+ metricFilter: options.metricFilter,
423
+ offset: options.offset ? String(options.offset) : void 0,
424
+ limit: options.limit ? String(options.limit) : void 0,
425
+ orderBys: options.orderBys,
426
+ keepEmptyRows: options.keepEmptyRows,
427
+ returnPropertyQuota: options.returnPropertyQuota
428
+ }
429
+ })).data;
430
+ return {
431
+ dimensionHeaders: (data.dimensionHeaders || []).map((h) => ({ name: h.name || "" })),
432
+ metricHeaders: (data.metricHeaders || []).map((h) => ({
433
+ name: h.name || "",
434
+ type: h.type || ""
435
+ })),
436
+ rows: (data.rows || []).map((r) => ({
437
+ dimensionValues: (r.dimensionValues || []).map((v) => ({ value: v.value || "" })),
438
+ metricValues: (r.metricValues || []).map((v) => ({ value: v.value || "" }))
439
+ })),
440
+ rowCount: data.rowCount ?? void 0,
441
+ metadata: data.metadata ? {
442
+ currencyCode: data.metadata.currencyCode ?? void 0,
443
+ timeZone: data.metadata.timeZone ?? void 0,
444
+ dataLossFromOtherRow: data.metadata.dataLossFromOtherRow ?? void 0,
445
+ emptyReason: data.metadata.emptyReason ?? void 0
446
+ } : void 0,
447
+ propertyQuota: data.propertyQuota ? {
448
+ tokensPerDay: {
449
+ consumed: data.propertyQuota.tokensPerDay?.consumed || 0,
450
+ remaining: data.propertyQuota.tokensPerDay?.remaining || 0
451
+ },
452
+ tokensPerHour: {
453
+ consumed: data.propertyQuota.tokensPerHour?.consumed || 0,
454
+ remaining: data.propertyQuota.tokensPerHour?.remaining || 0
455
+ },
456
+ concurrentRequests: {
457
+ consumed: data.propertyQuota.concurrentRequests?.consumed || 0,
458
+ remaining: data.propertyQuota.concurrentRequests?.remaining || 0
459
+ }
460
+ } : void 0
461
+ };
462
+ } catch (error) {
463
+ throw this.mapError(error);
464
+ }
465
+ }
466
+ async runRealtimeReport(propertyId, options) {
467
+ await this.ensureClients();
468
+ try {
469
+ const data = (await this.dataClient.properties.runRealtimeReport({
470
+ property: this.getPropertyName(propertyId),
471
+ requestBody: {
472
+ dimensions: options?.dimensions,
473
+ metrics: options?.metrics || [{ name: "activeUsers" }],
474
+ dimensionFilter: options?.dimensionFilter,
475
+ metricFilter: options?.metricFilter,
476
+ limit: options?.limit ? String(options.limit) : void 0,
477
+ minuteRanges: options?.minuteRanges
478
+ }
479
+ })).data;
480
+ return {
481
+ dimensionHeaders: (data.dimensionHeaders || []).map((h) => ({ name: h.name || "" })),
482
+ metricHeaders: (data.metricHeaders || []).map((h) => ({
483
+ name: h.name || "",
484
+ type: h.type || ""
485
+ })),
486
+ rows: (data.rows || []).map((r) => ({
487
+ dimensionValues: (r.dimensionValues || []).map((v) => ({ value: v.value || "" })),
488
+ metricValues: (r.metricValues || []).map((v) => ({ value: v.value || "" }))
489
+ })),
490
+ rowCount: data.rowCount ?? void 0
491
+ };
492
+ } catch (error) {
493
+ throw this.mapError(error);
494
+ }
495
+ }
496
+ async getMetrics(propertyId) {
497
+ await this.ensureClients();
498
+ try {
499
+ return ((await this.dataClient.properties.getMetadata({ name: `${this.getPropertyName(propertyId)}/metadata` })).data.metrics || []).map((m) => ({
500
+ apiName: m.apiName || "",
501
+ uiName: m.uiName || "",
502
+ description: m.description || "",
503
+ deprecatedApiNames: m.deprecatedApiNames ?? void 0,
504
+ type: m.type,
505
+ expression: m.expression ?? void 0,
506
+ customDefinition: m.customDefinition ?? void 0,
507
+ blockedReasons: m.blockedReasons ?? void 0,
508
+ category: m.category ?? void 0
509
+ }));
510
+ } catch (error) {
511
+ throw this.mapError(error);
512
+ }
513
+ }
514
+ async getDimensions(propertyId) {
515
+ await this.ensureClients();
516
+ try {
517
+ return ((await this.dataClient.properties.getMetadata({ name: `${this.getPropertyName(propertyId)}/metadata` })).data.dimensions || []).map((d) => ({
518
+ apiName: d.apiName || "",
519
+ uiName: d.uiName || "",
520
+ description: d.description || "",
521
+ deprecatedApiNames: d.deprecatedApiNames ?? void 0,
522
+ customDefinition: d.customDefinition ?? void 0,
523
+ category: d.category ?? void 0
524
+ }));
525
+ } catch (error) {
526
+ throw this.mapError(error);
527
+ }
528
+ }
529
+ async track(event) {
530
+ if (!this.options.measurementId || !this.options.apiSecret) throw new AnalyticsError("Measurement ID and API Secret are required for server-side tracking", "MISSING_CREDENTIALS", "ga4");
531
+ const clientId = event.clientId || this.generateClientId();
532
+ const url = `${MEASUREMENT_PROTOCOL_URL}?measurement_id=${this.options.measurementId}&api_secret=${this.options.apiSecret}`;
533
+ const payload = {
534
+ client_id: clientId,
535
+ user_id: event.userId,
536
+ timestamp_micros: event.timestamp,
537
+ non_personalized_ads: event.nonPersonalizedAds,
538
+ events: [{
539
+ name: event.name,
540
+ params: event.params
541
+ }]
542
+ };
543
+ try {
544
+ const response = await fetch(url, {
545
+ method: "POST",
546
+ headers: { "Content-Type": "application/json" },
547
+ body: JSON.stringify(payload)
548
+ });
549
+ if (!response.ok) throw new AnalyticsError(`Measurement Protocol error: ${response.status}`, "TRACKING_ERROR", "ga4");
550
+ } catch (error) {
551
+ if (error instanceof AnalyticsError) throw error;
552
+ throw this.mapError(error);
553
+ }
554
+ }
555
+ async trackPageview(pageview) {
556
+ await this.track({
557
+ name: "page_view",
558
+ params: {
559
+ page_path: pageview.pagePath,
560
+ page_title: pageview.pageTitle || "",
561
+ page_location: pageview.pageLocation || "",
562
+ ...pageview.params
563
+ },
564
+ clientId: pageview.clientId,
565
+ userId: pageview.userId
566
+ });
567
+ }
568
+ async trackBatch(events) {
569
+ if (!this.options.measurementId || !this.options.apiSecret) throw new AnalyticsError("Measurement ID and API Secret are required for server-side tracking", "MISSING_CREDENTIALS", "ga4");
570
+ const batchSize = 25;
571
+ for (let i = 0; i < events.length; i += batchSize) {
572
+ const batch = events.slice(i, i + batchSize);
573
+ const byClient = /* @__PURE__ */ new Map();
574
+ for (const event of batch) {
575
+ const clientId = event.clientId || this.generateClientId();
576
+ const existing = byClient.get(clientId) || [];
577
+ existing.push(event);
578
+ byClient.set(clientId, existing);
579
+ }
580
+ for (const [clientId, clientEvents] of byClient) {
581
+ const url = `${MEASUREMENT_PROTOCOL_URL}?measurement_id=${this.options.measurementId}&api_secret=${this.options.apiSecret}`;
582
+ const payload = {
583
+ client_id: clientId,
584
+ events: clientEvents.map((e) => ({
585
+ name: e.name,
586
+ params: e.params
587
+ }))
588
+ };
589
+ try {
590
+ const response = await fetch(url, {
591
+ method: "POST",
592
+ headers: { "Content-Type": "application/json" },
593
+ body: JSON.stringify(payload)
594
+ });
595
+ if (!response.ok) throw new AnalyticsError(`Measurement Protocol error: ${response.status}`, "TRACKING_ERROR", "ga4");
596
+ } catch (error) {
597
+ if (error instanceof AnalyticsError) throw error;
598
+ throw this.mapError(error);
599
+ }
600
+ }
601
+ }
602
+ }
603
+ async identify(userId, traits) {
604
+ await this.track({
605
+ name: "user_engagement",
606
+ userId,
607
+ params: traits
608
+ });
609
+ }
610
+ /**
611
+ * Generate a random client ID for Measurement Protocol
612
+ */
613
+ generateClientId() {
614
+ return `${Math.floor(Math.random() * 2147483647)}.${Math.floor(Date.now() / 1e3)}`;
615
+ }
616
+ generateTrackingSnippet(propertyId, options) {
617
+ const measurementId = this.options.measurementId || `G-${propertyId}`;
618
+ const configOptions = {};
619
+ if (options?.anonymizeIp) configOptions.anonymize_ip = true;
620
+ if (options?.sendPageView === false) configOptions.send_page_view = false;
621
+ if (options?.cookieFlags) configOptions.cookie_flags = options.cookieFlags;
622
+ if (options?.customConfig) Object.assign(configOptions, options.customConfig);
623
+ return {
624
+ html: `<!-- Google Analytics -->
625
+ <script async src="https://www.googletagmanager.com/gtag/js?id=${measurementId}"><\/script>
626
+ <script>
627
+ window.dataLayer = window.dataLayer || [];
628
+ function gtag(){dataLayer.push(arguments);}
629
+ gtag('js', new Date());
630
+ gtag('config', '${measurementId}'${Object.keys(configOptions).length > 0 ? `, ${JSON.stringify(configOptions)}` : ""});
631
+ <\/script>`,
632
+ config: {
633
+ measurementId,
634
+ ...configOptions
635
+ },
636
+ scripts: [`https://www.googletagmanager.com/gtag/js?id=${measurementId}`]
637
+ };
638
+ }
639
+ generateConfig(propertyId, options) {
640
+ const config = { measurement_id: this.options.measurementId || `G-${propertyId}` };
641
+ if (options?.anonymizeIp) config.anonymize_ip = true;
642
+ if (options?.sendPageView === false) config.send_page_view = false;
643
+ if (options?.userId) config.user_id = options.userId;
644
+ if (options?.customDimensions) Object.entries(options.customDimensions).forEach(([key, value]) => {
645
+ config[key] = value;
646
+ });
647
+ return config;
648
+ }
649
+ async getCapabilities() {
650
+ return {
651
+ propertyManagement: true,
652
+ dataStreams: true,
653
+ customDimensions: true,
654
+ customMetrics: true,
655
+ keyEvents: true,
656
+ reporting: true,
657
+ realtimeReporting: true,
658
+ serverSideTracking: true,
659
+ clientSideSnippet: true,
660
+ userIdentification: true,
661
+ batchTracking: true
662
+ };
663
+ }
664
+ };
665
+ //#endregion
666
+ export { GA4Provider };
667
+
668
+ //# sourceMappingURL=ga4-B69in3EQ.js.map