@happyvertical/analytics 0.74.8
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 +33 -0
- package/LICENSE +7 -0
- package/dist/chunks/ga4-6gyDPZRn.js +863 -0
- package/dist/chunks/ga4-6gyDPZRn.js.map +1 -0
- package/dist/chunks/matomo-Ds_oRmZ6.js +1043 -0
- package/dist/chunks/matomo-Ds_oRmZ6.js.map +1 -0
- package/dist/chunks/plausible-BxpNa6qF.js +479 -0
- package/dist/chunks/plausible-BxpNa6qF.js.map +1 -0
- package/dist/cli/claude-context.d.ts +3 -0
- package/dist/cli/claude-context.d.ts.map +1 -0
- package/dist/cli/claude-context.js +21 -0
- package/dist/cli/claude-context.js.map +1 -0
- package/dist/index.d.ts +37 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +135 -0
- package/dist/index.js.map +1 -0
- package/dist/shared/factory.d.ts +48 -0
- package/dist/shared/factory.d.ts.map +1 -0
- package/dist/shared/providers/ga4.d.ts +68 -0
- package/dist/shared/providers/ga4.d.ts.map +1 -0
- package/dist/shared/providers/matomo-admin.d.ts +56 -0
- package/dist/shared/providers/matomo-admin.d.ts.map +1 -0
- package/dist/shared/providers/matomo.d.ts +49 -0
- package/dist/shared/providers/matomo.d.ts.map +1 -0
- package/dist/shared/providers/plausible.d.ts +50 -0
- package/dist/shared/providers/plausible.d.ts.map +1 -0
- package/dist/shared/types.d.ts +1235 -0
- package/dist/shared/types.d.ts.map +1 -0
- package/metadata.json +30 -0
- package/package.json +57 -0
|
@@ -0,0 +1,863 @@
|
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
|
+
import { google } from "googleapis";
|
|
3
|
+
import { AuthenticationError, AnalyticsError, QuotaExceededError, RateLimitError, PropertyNotFoundError } from "../index.js";
|
|
4
|
+
const MEASUREMENT_PROTOCOL_URL = "https://www.google-analytics.com/mp/collect";
|
|
5
|
+
const PROPERTY_HYDRATION_CONCURRENCY = 5;
|
|
6
|
+
class GA4Provider {
|
|
7
|
+
adminClient = null;
|
|
8
|
+
dataClient = null;
|
|
9
|
+
options;
|
|
10
|
+
credentials = null;
|
|
11
|
+
constructor(options) {
|
|
12
|
+
this.options = {
|
|
13
|
+
timeout: 3e4,
|
|
14
|
+
maxRetries: 3,
|
|
15
|
+
cacheTTL: 36e5,
|
|
16
|
+
// 1 hour
|
|
17
|
+
...options
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Initialize Google API clients with service account credentials
|
|
22
|
+
*/
|
|
23
|
+
async ensureClients() {
|
|
24
|
+
if (this.adminClient && this.dataClient) {
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
if (!this.credentials && this.options.serviceAccountKey) {
|
|
28
|
+
if (typeof this.options.serviceAccountKey === "string") {
|
|
29
|
+
const content = await readFile(this.options.serviceAccountKey, "utf-8");
|
|
30
|
+
this.credentials = JSON.parse(content);
|
|
31
|
+
} else {
|
|
32
|
+
this.credentials = this.options.serviceAccountKey;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
if (!this.credentials) {
|
|
36
|
+
throw new AuthenticationError("ga4");
|
|
37
|
+
}
|
|
38
|
+
const auth = new google.auth.GoogleAuth({
|
|
39
|
+
credentials: this.credentials,
|
|
40
|
+
scopes: [
|
|
41
|
+
"https://www.googleapis.com/auth/analytics.readonly",
|
|
42
|
+
"https://www.googleapis.com/auth/analytics.edit"
|
|
43
|
+
]
|
|
44
|
+
});
|
|
45
|
+
this.adminClient = google.analyticsadmin({
|
|
46
|
+
version: "v1beta",
|
|
47
|
+
auth
|
|
48
|
+
});
|
|
49
|
+
this.dataClient = google.analyticsdata({
|
|
50
|
+
version: "v1beta",
|
|
51
|
+
auth
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Normalize property ID to numeric format
|
|
56
|
+
*/
|
|
57
|
+
normalizePropertyId(propertyId) {
|
|
58
|
+
return propertyId.replace(/^properties\//, "");
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Get full property resource name
|
|
62
|
+
*/
|
|
63
|
+
getPropertyName(propertyId) {
|
|
64
|
+
const id = this.normalizePropertyId(propertyId);
|
|
65
|
+
return `properties/${id}`;
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Map a Google Analytics property resource to the public Property shape
|
|
69
|
+
*/
|
|
70
|
+
mapProperty(property) {
|
|
71
|
+
return {
|
|
72
|
+
id: property.name?.replace("properties/", "") || "",
|
|
73
|
+
name: property.name || "",
|
|
74
|
+
displayName: property.displayName || "",
|
|
75
|
+
createTime: property.createTime || "",
|
|
76
|
+
updateTime: property.updateTime ?? void 0,
|
|
77
|
+
timeZone: property.timeZone ?? void 0,
|
|
78
|
+
currencyCode: property.currencyCode ?? void 0,
|
|
79
|
+
industryCategory: property.industryCategory ?? void 0,
|
|
80
|
+
serviceLevel: property.serviceLevel
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Hydrate discovered properties without firing unbounded concurrent requests
|
|
85
|
+
*/
|
|
86
|
+
async hydrateProperties(properties) {
|
|
87
|
+
const hydrated = [];
|
|
88
|
+
for (let index = 0; index < properties.length; index += PROPERTY_HYDRATION_CONCURRENCY) {
|
|
89
|
+
const batch = properties.slice(
|
|
90
|
+
index,
|
|
91
|
+
index + PROPERTY_HYDRATION_CONCURRENCY
|
|
92
|
+
);
|
|
93
|
+
const hydratedBatch = await Promise.all(
|
|
94
|
+
batch.map(async (property) => {
|
|
95
|
+
const response = await this.adminClient.properties.get({
|
|
96
|
+
name: property.name
|
|
97
|
+
});
|
|
98
|
+
return this.mapProperty(response.data);
|
|
99
|
+
})
|
|
100
|
+
);
|
|
101
|
+
hydrated.push(...hydratedBatch);
|
|
102
|
+
}
|
|
103
|
+
return hydrated;
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Map Google API errors to our error types
|
|
107
|
+
*/
|
|
108
|
+
mapError(error) {
|
|
109
|
+
if (error instanceof AnalyticsError) {
|
|
110
|
+
return error;
|
|
111
|
+
}
|
|
112
|
+
const err = error;
|
|
113
|
+
const status = err.code || err.status;
|
|
114
|
+
const message = err.message || "Unknown error";
|
|
115
|
+
switch (status) {
|
|
116
|
+
case 401:
|
|
117
|
+
case 403:
|
|
118
|
+
return new AuthenticationError("ga4");
|
|
119
|
+
case 404:
|
|
120
|
+
return new PropertyNotFoundError(message, "ga4");
|
|
121
|
+
case 429:
|
|
122
|
+
return new RateLimitError("ga4");
|
|
123
|
+
case 402:
|
|
124
|
+
return new QuotaExceededError("ga4");
|
|
125
|
+
default:
|
|
126
|
+
return new AnalyticsError(message, "API_ERROR", "ga4");
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
// ===========================================================================
|
|
130
|
+
// Property Management
|
|
131
|
+
// ===========================================================================
|
|
132
|
+
async createProperty(options) {
|
|
133
|
+
await this.ensureClients();
|
|
134
|
+
if (!options.parent) {
|
|
135
|
+
throw new AnalyticsError(
|
|
136
|
+
"Parent account is required for creating GA4 properties",
|
|
137
|
+
"MISSING_PARENT",
|
|
138
|
+
"ga4"
|
|
139
|
+
);
|
|
140
|
+
}
|
|
141
|
+
try {
|
|
142
|
+
const response = await this.adminClient.properties.create({
|
|
143
|
+
requestBody: {
|
|
144
|
+
displayName: options.displayName,
|
|
145
|
+
timeZone: options.timeZone || "America/Los_Angeles",
|
|
146
|
+
currencyCode: options.currencyCode || "USD",
|
|
147
|
+
industryCategory: options.industryCategory,
|
|
148
|
+
parent: options.parent
|
|
149
|
+
}
|
|
150
|
+
});
|
|
151
|
+
const property = this.mapProperty(response.data);
|
|
152
|
+
if (!property.createTime) {
|
|
153
|
+
property.createTime = (/* @__PURE__ */ new Date()).toISOString();
|
|
154
|
+
}
|
|
155
|
+
return property;
|
|
156
|
+
} catch (error) {
|
|
157
|
+
throw this.mapError(error);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
async listProperties(options) {
|
|
161
|
+
await this.ensureClients();
|
|
162
|
+
try {
|
|
163
|
+
const properties = /* @__PURE__ */ new Map();
|
|
164
|
+
let pageToken;
|
|
165
|
+
do {
|
|
166
|
+
const response = await this.adminClient.accountSummaries.list({
|
|
167
|
+
pageSize: 200,
|
|
168
|
+
pageToken
|
|
169
|
+
});
|
|
170
|
+
for (const accountSummary of response.data.accountSummaries || []) {
|
|
171
|
+
for (const propertySummary of accountSummary.propertySummaries || []) {
|
|
172
|
+
const propertyName = propertySummary.property || "";
|
|
173
|
+
if (!propertyName || properties.has(propertyName)) {
|
|
174
|
+
continue;
|
|
175
|
+
}
|
|
176
|
+
properties.set(propertyName, {
|
|
177
|
+
id: propertyName.replace("properties/", ""),
|
|
178
|
+
name: propertyName,
|
|
179
|
+
displayName: propertySummary.displayName || "",
|
|
180
|
+
createTime: ""
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
pageToken = response.data.nextPageToken ?? void 0;
|
|
185
|
+
} while (pageToken);
|
|
186
|
+
const listedProperties = [...properties.values()];
|
|
187
|
+
if (options?.hydrate === false) {
|
|
188
|
+
return listedProperties;
|
|
189
|
+
}
|
|
190
|
+
return this.hydrateProperties(listedProperties);
|
|
191
|
+
} catch (error) {
|
|
192
|
+
throw this.mapError(error);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
async getProperty(propertyId) {
|
|
196
|
+
await this.ensureClients();
|
|
197
|
+
try {
|
|
198
|
+
const response = await this.adminClient.properties.get({
|
|
199
|
+
name: this.getPropertyName(propertyId)
|
|
200
|
+
});
|
|
201
|
+
return this.mapProperty(response.data);
|
|
202
|
+
} catch (error) {
|
|
203
|
+
throw this.mapError(error);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
async updateProperty(propertyId, data) {
|
|
207
|
+
await this.ensureClients();
|
|
208
|
+
const updateMask = [];
|
|
209
|
+
if (data.displayName) updateMask.push("displayName");
|
|
210
|
+
if (data.timeZone) updateMask.push("timeZone");
|
|
211
|
+
if (data.currencyCode) updateMask.push("currencyCode");
|
|
212
|
+
if (data.industryCategory) updateMask.push("industryCategory");
|
|
213
|
+
try {
|
|
214
|
+
const response = await this.adminClient.properties.patch({
|
|
215
|
+
name: this.getPropertyName(propertyId),
|
|
216
|
+
updateMask: updateMask.join(","),
|
|
217
|
+
requestBody: {
|
|
218
|
+
displayName: data.displayName,
|
|
219
|
+
timeZone: data.timeZone,
|
|
220
|
+
currencyCode: data.currencyCode,
|
|
221
|
+
industryCategory: data.industryCategory
|
|
222
|
+
}
|
|
223
|
+
});
|
|
224
|
+
return this.mapProperty(response.data);
|
|
225
|
+
} catch (error) {
|
|
226
|
+
throw this.mapError(error);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
async deleteProperty(propertyId) {
|
|
230
|
+
await this.ensureClients();
|
|
231
|
+
try {
|
|
232
|
+
await this.adminClient.properties.delete({
|
|
233
|
+
name: this.getPropertyName(propertyId)
|
|
234
|
+
});
|
|
235
|
+
} catch (error) {
|
|
236
|
+
throw this.mapError(error);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
// ===========================================================================
|
|
240
|
+
// Data Streams
|
|
241
|
+
// ===========================================================================
|
|
242
|
+
async getDataStreams(propertyId) {
|
|
243
|
+
await this.ensureClients();
|
|
244
|
+
try {
|
|
245
|
+
const response = await this.adminClient.properties.dataStreams.list({
|
|
246
|
+
parent: this.getPropertyName(propertyId)
|
|
247
|
+
});
|
|
248
|
+
return (response.data.dataStreams || []).map(
|
|
249
|
+
(ds) => ({
|
|
250
|
+
id: ds.name?.split("/").pop() || "",
|
|
251
|
+
type: ds.type,
|
|
252
|
+
displayName: ds.displayName || "",
|
|
253
|
+
measurementId: ds.webStreamData?.measurementId ?? void 0,
|
|
254
|
+
firebaseAppId: ds.androidAppStreamData?.firebaseAppId ?? ds.iosAppStreamData?.firebaseAppId ?? void 0,
|
|
255
|
+
defaultUri: ds.webStreamData?.defaultUri ?? void 0,
|
|
256
|
+
createTime: ds.createTime || "",
|
|
257
|
+
updateTime: ds.updateTime ?? void 0
|
|
258
|
+
})
|
|
259
|
+
);
|
|
260
|
+
} catch (error) {
|
|
261
|
+
throw this.mapError(error);
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
async createDataStream(propertyId, options) {
|
|
265
|
+
await this.ensureClients();
|
|
266
|
+
try {
|
|
267
|
+
const requestBody = {
|
|
268
|
+
type: options.type,
|
|
269
|
+
displayName: options.displayName
|
|
270
|
+
};
|
|
271
|
+
if (options.type === "WEB_DATA_STREAM") {
|
|
272
|
+
requestBody.webStreamData = {
|
|
273
|
+
defaultUri: options.defaultUri
|
|
274
|
+
};
|
|
275
|
+
} else if (options.type === "ANDROID_APP_DATA_STREAM") {
|
|
276
|
+
requestBody.androidAppStreamData = {
|
|
277
|
+
packageName: options.packageName
|
|
278
|
+
};
|
|
279
|
+
} else if (options.type === "IOS_APP_DATA_STREAM") {
|
|
280
|
+
requestBody.iosAppStreamData = {
|
|
281
|
+
bundleId: options.bundleId
|
|
282
|
+
};
|
|
283
|
+
}
|
|
284
|
+
const response = await this.adminClient.properties.dataStreams.create({
|
|
285
|
+
parent: this.getPropertyName(propertyId),
|
|
286
|
+
requestBody
|
|
287
|
+
});
|
|
288
|
+
const ds = response.data;
|
|
289
|
+
return {
|
|
290
|
+
id: ds.name?.split("/").pop() || "",
|
|
291
|
+
type: ds.type,
|
|
292
|
+
displayName: ds.displayName || "",
|
|
293
|
+
measurementId: ds.webStreamData?.measurementId ?? void 0,
|
|
294
|
+
firebaseAppId: ds.androidAppStreamData?.firebaseAppId ?? ds.iosAppStreamData?.firebaseAppId ?? void 0,
|
|
295
|
+
defaultUri: ds.webStreamData?.defaultUri ?? void 0,
|
|
296
|
+
createTime: ds.createTime || "",
|
|
297
|
+
updateTime: ds.updateTime ?? void 0
|
|
298
|
+
};
|
|
299
|
+
} catch (error) {
|
|
300
|
+
throw this.mapError(error);
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
async deleteDataStream(propertyId, streamId) {
|
|
304
|
+
await this.ensureClients();
|
|
305
|
+
try {
|
|
306
|
+
await this.adminClient.properties.dataStreams.delete({
|
|
307
|
+
name: `${this.getPropertyName(propertyId)}/dataStreams/${streamId}`
|
|
308
|
+
});
|
|
309
|
+
} catch (error) {
|
|
310
|
+
throw this.mapError(error);
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
// ===========================================================================
|
|
314
|
+
// Custom Definitions
|
|
315
|
+
// ===========================================================================
|
|
316
|
+
async getCustomDimensions(propertyId) {
|
|
317
|
+
await this.ensureClients();
|
|
318
|
+
try {
|
|
319
|
+
const response = await this.adminClient.properties.customDimensions.list(
|
|
320
|
+
{
|
|
321
|
+
parent: this.getPropertyName(propertyId)
|
|
322
|
+
}
|
|
323
|
+
);
|
|
324
|
+
return (response.data.customDimensions || []).map(
|
|
325
|
+
(cd) => ({
|
|
326
|
+
id: cd.name?.split("/").pop() || "",
|
|
327
|
+
name: cd.name || "",
|
|
328
|
+
parameterName: cd.parameterName || "",
|
|
329
|
+
displayName: cd.displayName || "",
|
|
330
|
+
description: cd.description ?? void 0,
|
|
331
|
+
scope: cd.scope,
|
|
332
|
+
disallowAdsPersonalization: cd.disallowAdsPersonalization ?? void 0
|
|
333
|
+
})
|
|
334
|
+
);
|
|
335
|
+
} catch (error) {
|
|
336
|
+
throw this.mapError(error);
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
async createCustomDimension(propertyId, options) {
|
|
340
|
+
await this.ensureClients();
|
|
341
|
+
try {
|
|
342
|
+
const response = await this.adminClient.properties.customDimensions.create({
|
|
343
|
+
parent: this.getPropertyName(propertyId),
|
|
344
|
+
requestBody: {
|
|
345
|
+
parameterName: options.parameterName,
|
|
346
|
+
displayName: options.displayName,
|
|
347
|
+
description: options.description,
|
|
348
|
+
scope: options.scope,
|
|
349
|
+
disallowAdsPersonalization: options.disallowAdsPersonalization
|
|
350
|
+
}
|
|
351
|
+
});
|
|
352
|
+
const cd = response.data;
|
|
353
|
+
return {
|
|
354
|
+
id: cd.name?.split("/").pop() || "",
|
|
355
|
+
name: cd.name || "",
|
|
356
|
+
parameterName: cd.parameterName || "",
|
|
357
|
+
displayName: cd.displayName || "",
|
|
358
|
+
description: cd.description ?? void 0,
|
|
359
|
+
scope: cd.scope,
|
|
360
|
+
disallowAdsPersonalization: cd.disallowAdsPersonalization ?? void 0
|
|
361
|
+
};
|
|
362
|
+
} catch (error) {
|
|
363
|
+
throw this.mapError(error);
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
async archiveCustomDimension(propertyId, dimensionId) {
|
|
367
|
+
await this.ensureClients();
|
|
368
|
+
try {
|
|
369
|
+
await this.adminClient.properties.customDimensions.archive({
|
|
370
|
+
name: `${this.getPropertyName(propertyId)}/customDimensions/${dimensionId}`
|
|
371
|
+
});
|
|
372
|
+
} catch (error) {
|
|
373
|
+
throw this.mapError(error);
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
async getCustomMetrics(propertyId) {
|
|
377
|
+
await this.ensureClients();
|
|
378
|
+
try {
|
|
379
|
+
const response = await this.adminClient.properties.customMetrics.list({
|
|
380
|
+
parent: this.getPropertyName(propertyId)
|
|
381
|
+
});
|
|
382
|
+
return (response.data.customMetrics || []).map(
|
|
383
|
+
(cm) => ({
|
|
384
|
+
id: cm.name?.split("/").pop() || "",
|
|
385
|
+
name: cm.name || "",
|
|
386
|
+
parameterName: cm.parameterName || "",
|
|
387
|
+
displayName: cm.displayName || "",
|
|
388
|
+
description: cm.description ?? void 0,
|
|
389
|
+
scope: "EVENT",
|
|
390
|
+
measurementUnit: cm.measurementUnit,
|
|
391
|
+
restrictedMetricType: cm.restrictedMetricType?.[0] ?? void 0
|
|
392
|
+
})
|
|
393
|
+
);
|
|
394
|
+
} catch (error) {
|
|
395
|
+
throw this.mapError(error);
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
async createCustomMetric(propertyId, options) {
|
|
399
|
+
await this.ensureClients();
|
|
400
|
+
try {
|
|
401
|
+
const response = await this.adminClient.properties.customMetrics.create({
|
|
402
|
+
parent: this.getPropertyName(propertyId),
|
|
403
|
+
requestBody: {
|
|
404
|
+
parameterName: options.parameterName,
|
|
405
|
+
displayName: options.displayName,
|
|
406
|
+
description: options.description,
|
|
407
|
+
measurementUnit: options.measurementUnit,
|
|
408
|
+
restrictedMetricType: options.restrictedMetricType ? [options.restrictedMetricType] : void 0,
|
|
409
|
+
scope: "EVENT"
|
|
410
|
+
}
|
|
411
|
+
});
|
|
412
|
+
const cm = response.data;
|
|
413
|
+
return {
|
|
414
|
+
id: cm.name?.split("/").pop() || "",
|
|
415
|
+
name: cm.name || "",
|
|
416
|
+
parameterName: cm.parameterName || "",
|
|
417
|
+
displayName: cm.displayName || "",
|
|
418
|
+
description: cm.description ?? void 0,
|
|
419
|
+
scope: "EVENT",
|
|
420
|
+
measurementUnit: cm.measurementUnit,
|
|
421
|
+
restrictedMetricType: cm.restrictedMetricType?.[0] ?? void 0
|
|
422
|
+
};
|
|
423
|
+
} catch (error) {
|
|
424
|
+
throw this.mapError(error);
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
async archiveCustomMetric(propertyId, metricId) {
|
|
428
|
+
await this.ensureClients();
|
|
429
|
+
try {
|
|
430
|
+
await this.adminClient.properties.customMetrics.archive({
|
|
431
|
+
name: `${this.getPropertyName(propertyId)}/customMetrics/${metricId}`
|
|
432
|
+
});
|
|
433
|
+
} catch (error) {
|
|
434
|
+
throw this.mapError(error);
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
// ===========================================================================
|
|
438
|
+
// Key Events (Conversions)
|
|
439
|
+
// ===========================================================================
|
|
440
|
+
async getKeyEvents(propertyId) {
|
|
441
|
+
await this.ensureClients();
|
|
442
|
+
try {
|
|
443
|
+
const response = await this.adminClient.properties.keyEvents.list({
|
|
444
|
+
parent: this.getPropertyName(propertyId)
|
|
445
|
+
});
|
|
446
|
+
return (response.data.keyEvents || []).map(
|
|
447
|
+
(ke) => ({
|
|
448
|
+
id: ke.name?.split("/").pop() || "",
|
|
449
|
+
name: ke.name || "",
|
|
450
|
+
eventName: ke.eventName || "",
|
|
451
|
+
createTime: ke.createTime || "",
|
|
452
|
+
countingMethod: ke.countingMethod,
|
|
453
|
+
defaultValue: ke.defaultValue ? {
|
|
454
|
+
numericValue: ke.defaultValue.numericValue ?? void 0,
|
|
455
|
+
currencyCode: ke.defaultValue.currencyCode ?? void 0
|
|
456
|
+
} : void 0
|
|
457
|
+
})
|
|
458
|
+
);
|
|
459
|
+
} catch (error) {
|
|
460
|
+
throw this.mapError(error);
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
async createKeyEvent(propertyId, options) {
|
|
464
|
+
await this.ensureClients();
|
|
465
|
+
try {
|
|
466
|
+
const response = await this.adminClient.properties.keyEvents.create({
|
|
467
|
+
parent: this.getPropertyName(propertyId),
|
|
468
|
+
requestBody: {
|
|
469
|
+
eventName: options.eventName,
|
|
470
|
+
countingMethod: options.countingMethod,
|
|
471
|
+
defaultValue: options.defaultValue
|
|
472
|
+
}
|
|
473
|
+
});
|
|
474
|
+
const ke = response.data;
|
|
475
|
+
return {
|
|
476
|
+
id: ke.name?.split("/").pop() || "",
|
|
477
|
+
name: ke.name || "",
|
|
478
|
+
eventName: ke.eventName || "",
|
|
479
|
+
createTime: ke.createTime || "",
|
|
480
|
+
countingMethod: ke.countingMethod,
|
|
481
|
+
defaultValue: ke.defaultValue ? {
|
|
482
|
+
numericValue: ke.defaultValue.numericValue ?? void 0,
|
|
483
|
+
currencyCode: ke.defaultValue.currencyCode ?? void 0
|
|
484
|
+
} : void 0
|
|
485
|
+
};
|
|
486
|
+
} catch (error) {
|
|
487
|
+
throw this.mapError(error);
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
async deleteKeyEvent(propertyId, eventId) {
|
|
491
|
+
await this.ensureClients();
|
|
492
|
+
try {
|
|
493
|
+
await this.adminClient.properties.keyEvents.delete({
|
|
494
|
+
name: `${this.getPropertyName(propertyId)}/keyEvents/${eventId}`
|
|
495
|
+
});
|
|
496
|
+
} catch (error) {
|
|
497
|
+
throw this.mapError(error);
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
// ===========================================================================
|
|
501
|
+
// Reporting
|
|
502
|
+
// ===========================================================================
|
|
503
|
+
async runReport(propertyId, options) {
|
|
504
|
+
await this.ensureClients();
|
|
505
|
+
try {
|
|
506
|
+
const response = await this.dataClient.properties.runReport({
|
|
507
|
+
property: this.getPropertyName(propertyId),
|
|
508
|
+
requestBody: {
|
|
509
|
+
dateRanges: options.dateRanges,
|
|
510
|
+
dimensions: options.dimensions,
|
|
511
|
+
metrics: options.metrics,
|
|
512
|
+
dimensionFilter: options.dimensionFilter,
|
|
513
|
+
metricFilter: options.metricFilter,
|
|
514
|
+
offset: options.offset ? String(options.offset) : void 0,
|
|
515
|
+
limit: options.limit ? String(options.limit) : void 0,
|
|
516
|
+
orderBys: options.orderBys,
|
|
517
|
+
keepEmptyRows: options.keepEmptyRows,
|
|
518
|
+
returnPropertyQuota: options.returnPropertyQuota
|
|
519
|
+
}
|
|
520
|
+
});
|
|
521
|
+
const data = response.data;
|
|
522
|
+
return {
|
|
523
|
+
dimensionHeaders: (data.dimensionHeaders || []).map(
|
|
524
|
+
(h) => ({
|
|
525
|
+
name: h.name || ""
|
|
526
|
+
})
|
|
527
|
+
),
|
|
528
|
+
metricHeaders: (data.metricHeaders || []).map(
|
|
529
|
+
(h) => ({
|
|
530
|
+
name: h.name || "",
|
|
531
|
+
type: h.type || ""
|
|
532
|
+
})
|
|
533
|
+
),
|
|
534
|
+
rows: (data.rows || []).map((r) => ({
|
|
535
|
+
dimensionValues: (r.dimensionValues || []).map(
|
|
536
|
+
(v) => ({
|
|
537
|
+
value: v.value || ""
|
|
538
|
+
})
|
|
539
|
+
),
|
|
540
|
+
metricValues: (r.metricValues || []).map(
|
|
541
|
+
(v) => ({
|
|
542
|
+
value: v.value || ""
|
|
543
|
+
})
|
|
544
|
+
)
|
|
545
|
+
})),
|
|
546
|
+
rowCount: data.rowCount ?? void 0,
|
|
547
|
+
metadata: data.metadata ? {
|
|
548
|
+
currencyCode: data.metadata.currencyCode ?? void 0,
|
|
549
|
+
timeZone: data.metadata.timeZone ?? void 0,
|
|
550
|
+
dataLossFromOtherRow: data.metadata.dataLossFromOtherRow ?? void 0,
|
|
551
|
+
emptyReason: data.metadata.emptyReason ?? void 0
|
|
552
|
+
} : void 0,
|
|
553
|
+
propertyQuota: data.propertyQuota ? {
|
|
554
|
+
tokensPerDay: {
|
|
555
|
+
consumed: data.propertyQuota.tokensPerDay?.consumed || 0,
|
|
556
|
+
remaining: data.propertyQuota.tokensPerDay?.remaining || 0
|
|
557
|
+
},
|
|
558
|
+
tokensPerHour: {
|
|
559
|
+
consumed: data.propertyQuota.tokensPerHour?.consumed || 0,
|
|
560
|
+
remaining: data.propertyQuota.tokensPerHour?.remaining || 0
|
|
561
|
+
},
|
|
562
|
+
concurrentRequests: {
|
|
563
|
+
consumed: data.propertyQuota.concurrentRequests?.consumed || 0,
|
|
564
|
+
remaining: data.propertyQuota.concurrentRequests?.remaining || 0
|
|
565
|
+
}
|
|
566
|
+
} : void 0
|
|
567
|
+
};
|
|
568
|
+
} catch (error) {
|
|
569
|
+
throw this.mapError(error);
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
async runRealtimeReport(propertyId, options) {
|
|
573
|
+
await this.ensureClients();
|
|
574
|
+
try {
|
|
575
|
+
const response = await this.dataClient.properties.runRealtimeReport({
|
|
576
|
+
property: this.getPropertyName(propertyId),
|
|
577
|
+
requestBody: {
|
|
578
|
+
dimensions: options?.dimensions,
|
|
579
|
+
metrics: options?.metrics || [{ name: "activeUsers" }],
|
|
580
|
+
dimensionFilter: options?.dimensionFilter,
|
|
581
|
+
metricFilter: options?.metricFilter,
|
|
582
|
+
limit: options?.limit ? String(options.limit) : void 0,
|
|
583
|
+
minuteRanges: options?.minuteRanges
|
|
584
|
+
}
|
|
585
|
+
});
|
|
586
|
+
const data = response.data;
|
|
587
|
+
return {
|
|
588
|
+
dimensionHeaders: (data.dimensionHeaders || []).map(
|
|
589
|
+
(h) => ({
|
|
590
|
+
name: h.name || ""
|
|
591
|
+
})
|
|
592
|
+
),
|
|
593
|
+
metricHeaders: (data.metricHeaders || []).map(
|
|
594
|
+
(h) => ({
|
|
595
|
+
name: h.name || "",
|
|
596
|
+
type: h.type || ""
|
|
597
|
+
})
|
|
598
|
+
),
|
|
599
|
+
rows: (data.rows || []).map((r) => ({
|
|
600
|
+
dimensionValues: (r.dimensionValues || []).map(
|
|
601
|
+
(v) => ({
|
|
602
|
+
value: v.value || ""
|
|
603
|
+
})
|
|
604
|
+
),
|
|
605
|
+
metricValues: (r.metricValues || []).map(
|
|
606
|
+
(v) => ({
|
|
607
|
+
value: v.value || ""
|
|
608
|
+
})
|
|
609
|
+
)
|
|
610
|
+
})),
|
|
611
|
+
rowCount: data.rowCount ?? void 0
|
|
612
|
+
};
|
|
613
|
+
} catch (error) {
|
|
614
|
+
throw this.mapError(error);
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
async getMetrics(propertyId) {
|
|
618
|
+
await this.ensureClients();
|
|
619
|
+
try {
|
|
620
|
+
const response = await this.dataClient.properties.getMetadata({
|
|
621
|
+
name: `${this.getPropertyName(propertyId)}/metadata`
|
|
622
|
+
});
|
|
623
|
+
return (response.data.metrics || []).map(
|
|
624
|
+
(m) => ({
|
|
625
|
+
apiName: m.apiName || "",
|
|
626
|
+
uiName: m.uiName || "",
|
|
627
|
+
description: m.description || "",
|
|
628
|
+
deprecatedApiNames: m.deprecatedApiNames ?? void 0,
|
|
629
|
+
type: m.type,
|
|
630
|
+
expression: m.expression ?? void 0,
|
|
631
|
+
customDefinition: m.customDefinition ?? void 0,
|
|
632
|
+
blockedReasons: m.blockedReasons ?? void 0,
|
|
633
|
+
category: m.category ?? void 0
|
|
634
|
+
})
|
|
635
|
+
);
|
|
636
|
+
} catch (error) {
|
|
637
|
+
throw this.mapError(error);
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
async getDimensions(propertyId) {
|
|
641
|
+
await this.ensureClients();
|
|
642
|
+
try {
|
|
643
|
+
const response = await this.dataClient.properties.getMetadata({
|
|
644
|
+
name: `${this.getPropertyName(propertyId)}/metadata`
|
|
645
|
+
});
|
|
646
|
+
return (response.data.dimensions || []).map(
|
|
647
|
+
(d) => ({
|
|
648
|
+
apiName: d.apiName || "",
|
|
649
|
+
uiName: d.uiName || "",
|
|
650
|
+
description: d.description || "",
|
|
651
|
+
deprecatedApiNames: d.deprecatedApiNames ?? void 0,
|
|
652
|
+
customDefinition: d.customDefinition ?? void 0,
|
|
653
|
+
category: d.category ?? void 0
|
|
654
|
+
})
|
|
655
|
+
);
|
|
656
|
+
} catch (error) {
|
|
657
|
+
throw this.mapError(error);
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
// ===========================================================================
|
|
661
|
+
// Event Tracking (Measurement Protocol)
|
|
662
|
+
// ===========================================================================
|
|
663
|
+
async track(event) {
|
|
664
|
+
if (!this.options.measurementId || !this.options.apiSecret) {
|
|
665
|
+
throw new AnalyticsError(
|
|
666
|
+
"Measurement ID and API Secret are required for server-side tracking",
|
|
667
|
+
"MISSING_CREDENTIALS",
|
|
668
|
+
"ga4"
|
|
669
|
+
);
|
|
670
|
+
}
|
|
671
|
+
const clientId = event.clientId || this.generateClientId();
|
|
672
|
+
const url = `${MEASUREMENT_PROTOCOL_URL}?measurement_id=${this.options.measurementId}&api_secret=${this.options.apiSecret}`;
|
|
673
|
+
const payload = {
|
|
674
|
+
client_id: clientId,
|
|
675
|
+
user_id: event.userId,
|
|
676
|
+
timestamp_micros: event.timestamp,
|
|
677
|
+
non_personalized_ads: event.nonPersonalizedAds,
|
|
678
|
+
events: [
|
|
679
|
+
{
|
|
680
|
+
name: event.name,
|
|
681
|
+
params: event.params
|
|
682
|
+
}
|
|
683
|
+
]
|
|
684
|
+
};
|
|
685
|
+
try {
|
|
686
|
+
const response = await fetch(url, {
|
|
687
|
+
method: "POST",
|
|
688
|
+
headers: {
|
|
689
|
+
"Content-Type": "application/json"
|
|
690
|
+
},
|
|
691
|
+
body: JSON.stringify(payload)
|
|
692
|
+
});
|
|
693
|
+
if (!response.ok) {
|
|
694
|
+
throw new AnalyticsError(
|
|
695
|
+
`Measurement Protocol error: ${response.status}`,
|
|
696
|
+
"TRACKING_ERROR",
|
|
697
|
+
"ga4"
|
|
698
|
+
);
|
|
699
|
+
}
|
|
700
|
+
} catch (error) {
|
|
701
|
+
if (error instanceof AnalyticsError) {
|
|
702
|
+
throw error;
|
|
703
|
+
}
|
|
704
|
+
throw this.mapError(error);
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
async trackPageview(pageview) {
|
|
708
|
+
await this.track({
|
|
709
|
+
name: "page_view",
|
|
710
|
+
params: {
|
|
711
|
+
page_path: pageview.pagePath,
|
|
712
|
+
page_title: pageview.pageTitle || "",
|
|
713
|
+
page_location: pageview.pageLocation || "",
|
|
714
|
+
...pageview.params
|
|
715
|
+
},
|
|
716
|
+
clientId: pageview.clientId,
|
|
717
|
+
userId: pageview.userId
|
|
718
|
+
});
|
|
719
|
+
}
|
|
720
|
+
async trackBatch(events) {
|
|
721
|
+
if (!this.options.measurementId || !this.options.apiSecret) {
|
|
722
|
+
throw new AnalyticsError(
|
|
723
|
+
"Measurement ID and API Secret are required for server-side tracking",
|
|
724
|
+
"MISSING_CREDENTIALS",
|
|
725
|
+
"ga4"
|
|
726
|
+
);
|
|
727
|
+
}
|
|
728
|
+
const batchSize = 25;
|
|
729
|
+
for (let i = 0; i < events.length; i += batchSize) {
|
|
730
|
+
const batch = events.slice(i, i + batchSize);
|
|
731
|
+
const byClient = /* @__PURE__ */ new Map();
|
|
732
|
+
for (const event of batch) {
|
|
733
|
+
const clientId = event.clientId || this.generateClientId();
|
|
734
|
+
const existing = byClient.get(clientId) || [];
|
|
735
|
+
existing.push(event);
|
|
736
|
+
byClient.set(clientId, existing);
|
|
737
|
+
}
|
|
738
|
+
for (const [clientId, clientEvents] of byClient) {
|
|
739
|
+
const url = `${MEASUREMENT_PROTOCOL_URL}?measurement_id=${this.options.measurementId}&api_secret=${this.options.apiSecret}`;
|
|
740
|
+
const payload = {
|
|
741
|
+
client_id: clientId,
|
|
742
|
+
events: clientEvents.map((e) => ({
|
|
743
|
+
name: e.name,
|
|
744
|
+
params: e.params
|
|
745
|
+
}))
|
|
746
|
+
};
|
|
747
|
+
try {
|
|
748
|
+
const response = await fetch(url, {
|
|
749
|
+
method: "POST",
|
|
750
|
+
headers: {
|
|
751
|
+
"Content-Type": "application/json"
|
|
752
|
+
},
|
|
753
|
+
body: JSON.stringify(payload)
|
|
754
|
+
});
|
|
755
|
+
if (!response.ok) {
|
|
756
|
+
throw new AnalyticsError(
|
|
757
|
+
`Measurement Protocol error: ${response.status}`,
|
|
758
|
+
"TRACKING_ERROR",
|
|
759
|
+
"ga4"
|
|
760
|
+
);
|
|
761
|
+
}
|
|
762
|
+
} catch (error) {
|
|
763
|
+
if (error instanceof AnalyticsError) {
|
|
764
|
+
throw error;
|
|
765
|
+
}
|
|
766
|
+
throw this.mapError(error);
|
|
767
|
+
}
|
|
768
|
+
}
|
|
769
|
+
}
|
|
770
|
+
}
|
|
771
|
+
async identify(userId, traits) {
|
|
772
|
+
await this.track({
|
|
773
|
+
name: "user_engagement",
|
|
774
|
+
userId,
|
|
775
|
+
params: traits
|
|
776
|
+
});
|
|
777
|
+
}
|
|
778
|
+
/**
|
|
779
|
+
* Generate a random client ID for Measurement Protocol
|
|
780
|
+
*/
|
|
781
|
+
generateClientId() {
|
|
782
|
+
return `${Math.floor(Math.random() * 2147483647)}.${Math.floor(Date.now() / 1e3)}`;
|
|
783
|
+
}
|
|
784
|
+
// ===========================================================================
|
|
785
|
+
// Client-Side Helpers
|
|
786
|
+
// ===========================================================================
|
|
787
|
+
generateTrackingSnippet(propertyId, options) {
|
|
788
|
+
const measurementId = this.options.measurementId || `G-${propertyId}`;
|
|
789
|
+
const configOptions = {};
|
|
790
|
+
if (options?.anonymizeIp) {
|
|
791
|
+
configOptions.anonymize_ip = true;
|
|
792
|
+
}
|
|
793
|
+
if (options?.sendPageView === false) {
|
|
794
|
+
configOptions.send_page_view = false;
|
|
795
|
+
}
|
|
796
|
+
if (options?.cookieFlags) {
|
|
797
|
+
configOptions.cookie_flags = options.cookieFlags;
|
|
798
|
+
}
|
|
799
|
+
if (options?.customConfig) {
|
|
800
|
+
Object.assign(configOptions, options.customConfig);
|
|
801
|
+
}
|
|
802
|
+
const configStr = Object.keys(configOptions).length > 0 ? `, ${JSON.stringify(configOptions)}` : "";
|
|
803
|
+
const html = `<!-- Google Analytics -->
|
|
804
|
+
<script async src="https://www.googletagmanager.com/gtag/js?id=${measurementId}"><\/script>
|
|
805
|
+
<script>
|
|
806
|
+
window.dataLayer = window.dataLayer || [];
|
|
807
|
+
function gtag(){dataLayer.push(arguments);}
|
|
808
|
+
gtag('js', new Date());
|
|
809
|
+
gtag('config', '${measurementId}'${configStr});
|
|
810
|
+
<\/script>`;
|
|
811
|
+
return {
|
|
812
|
+
html,
|
|
813
|
+
config: {
|
|
814
|
+
measurementId,
|
|
815
|
+
...configOptions
|
|
816
|
+
},
|
|
817
|
+
scripts: [`https://www.googletagmanager.com/gtag/js?id=${measurementId}`]
|
|
818
|
+
};
|
|
819
|
+
}
|
|
820
|
+
generateConfig(propertyId, options) {
|
|
821
|
+
const measurementId = this.options.measurementId || `G-${propertyId}`;
|
|
822
|
+
const config = {
|
|
823
|
+
measurement_id: measurementId
|
|
824
|
+
};
|
|
825
|
+
if (options?.anonymizeIp) {
|
|
826
|
+
config.anonymize_ip = true;
|
|
827
|
+
}
|
|
828
|
+
if (options?.sendPageView === false) {
|
|
829
|
+
config.send_page_view = false;
|
|
830
|
+
}
|
|
831
|
+
if (options?.userId) {
|
|
832
|
+
config.user_id = options.userId;
|
|
833
|
+
}
|
|
834
|
+
if (options?.customDimensions) {
|
|
835
|
+
Object.entries(options.customDimensions).forEach(([key, value]) => {
|
|
836
|
+
config[key] = value;
|
|
837
|
+
});
|
|
838
|
+
}
|
|
839
|
+
return config;
|
|
840
|
+
}
|
|
841
|
+
// ===========================================================================
|
|
842
|
+
// Provider Info
|
|
843
|
+
// ===========================================================================
|
|
844
|
+
async getCapabilities() {
|
|
845
|
+
return {
|
|
846
|
+
propertyManagement: true,
|
|
847
|
+
dataStreams: true,
|
|
848
|
+
customDimensions: true,
|
|
849
|
+
customMetrics: true,
|
|
850
|
+
keyEvents: true,
|
|
851
|
+
reporting: true,
|
|
852
|
+
realtimeReporting: true,
|
|
853
|
+
serverSideTracking: true,
|
|
854
|
+
clientSideSnippet: true,
|
|
855
|
+
userIdentification: true,
|
|
856
|
+
batchTracking: true
|
|
857
|
+
};
|
|
858
|
+
}
|
|
859
|
+
}
|
|
860
|
+
export {
|
|
861
|
+
GA4Provider
|
|
862
|
+
};
|
|
863
|
+
//# sourceMappingURL=ga4-6gyDPZRn.js.map
|