@happyvertical/analytics 0.80.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/dist/chunks/ga4-B69in3EQ.js +668 -0
- package/dist/chunks/ga4-B69in3EQ.js.map +1 -0
- package/dist/chunks/matomo-CUUG_Drb.js +853 -0
- package/dist/chunks/matomo-CUUG_Drb.js.map +1 -0
- package/dist/chunks/plausible-dXl_rz_8.js +396 -0
- package/dist/chunks/plausible-dXl_rz_8.js.map +1 -0
- package/dist/chunks/types-zsbKROGX.js +93 -0
- package/dist/chunks/types-zsbKROGX.js.map +1 -0
- package/dist/cli/claude-context.js +17 -17
- package/dist/cli/claude-context.js.map +1 -1
- package/dist/index.js +104 -127
- package/dist/index.js.map +1 -1
- package/package.json +5 -5
- package/dist/chunks/ga4-6gyDPZRn.js +0 -863
- package/dist/chunks/ga4-6gyDPZRn.js.map +0 -1
- package/dist/chunks/matomo-Ds_oRmZ6.js +0 -1043
- package/dist/chunks/matomo-Ds_oRmZ6.js.map +0 -1
- package/dist/chunks/plausible-BxpNa6qF.js +0 -479
- package/dist/chunks/plausible-BxpNa6qF.js.map +0 -1
|
@@ -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
|