@useairfoil/producer-google-analytics 0.0.1 → 0.0.2

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.
@@ -0,0 +1,711 @@
1
+ import { t as __exportAll } from "./rolldown-runtime-D7D4PA-g.mjs";
2
+ import { GoogleAnalyticsConfigDef, manifest } from "./manifest.mjs";
3
+ import { Auth, Connector, ConnectorError, Cursor, Fetch, Iceberg, Metrics, Resource, Telemetry } from "@useairfoil/connector-kit";
4
+ import { Clock, Config, Context, Data, DateTime, Duration, Effect, Exit, Layer, Option, Redacted, Schedule, Schema, SchemaTransformation, Semaphore } from "effect";
5
+ import { HttpClient, HttpClientRequest, HttpClientResponse } from "effect/http";
6
+ import { createPrivateKey, sign } from "node:crypto";
7
+ //#region src/client/auth.ts
8
+ var auth_exports = /* @__PURE__ */ __exportAll({
9
+ GoogleAnalyticsAuth: () => GoogleAnalyticsAuth,
10
+ layer: () => layer$2,
11
+ make: () => make$2,
12
+ makeAssertion: () => makeAssertion,
13
+ scope: () => scope,
14
+ tokenUrl: () => tokenUrl
15
+ });
16
+ var GoogleAnalyticsAuth = class extends Context.Service()("@useairfoil/producer-google-analytics/GoogleAnalyticsAuth") {};
17
+ const tokenUrl = "https://oauth2.googleapis.com/token";
18
+ const scope = "https://www.googleapis.com/auth/analytics.readonly";
19
+ const TokenResponseSchema = Schema.Struct({
20
+ access_token: Schema.String,
21
+ expires_in: Schema.Number
22
+ });
23
+ const TokenErrorSchema = Schema.Struct({
24
+ error: Schema.optional(Schema.String),
25
+ error_description: Schema.optional(Schema.String)
26
+ });
27
+ const base64Url = (value) => Buffer.from(value).toString("base64url");
28
+ /** Signs the service account JWT with RS256. Google allows at most 1 hour. */
29
+ const makeAssertion = (options) => Effect.try({
30
+ try: () => {
31
+ const now = Math.floor(options.nowMillis / 1e3);
32
+ const signingInput = `${base64Url(JSON.stringify({
33
+ alg: "RS256",
34
+ typ: "JWT"
35
+ }))}.${base64Url(JSON.stringify({
36
+ iss: options.clientEmail,
37
+ scope,
38
+ aud: tokenUrl,
39
+ iat: now,
40
+ exp: now + 3600
41
+ }))}`;
42
+ const key = createPrivateKey(options.privateKey.replace(/\\n/g, "\n"));
43
+ return `${signingInput}.${base64Url(sign("sha256", Buffer.from(signingInput), key))}`;
44
+ },
45
+ catch: (cause) => new ConnectorError({
46
+ message: "Invalid Google service account private key",
47
+ cause
48
+ })
49
+ });
50
+ const tokenError = (response) => HttpClientResponse.schemaBodyJson(TokenErrorSchema)(response).pipe(Effect.map((body) => body.error), Effect.orElseSucceed(() => void 0), Effect.flatMap((error) => Effect.fail(new ConnectorError({ message: error === "invalid_grant" || error === "invalid_client" ? "Google rejected the service account key" : `Google returned ${response.status} for the OAuth token` }))));
51
+ const make$2 = Effect.fnUntraced(function* (config) {
52
+ const client = (yield* HttpClient.HttpClient).pipe(HttpClient.retryTransient({
53
+ schedule: Schedule.exponential("250 millis").pipe(Schedule.jittered),
54
+ times: 3
55
+ }));
56
+ return yield* Auth.makeTokenCache({ acquire: Effect.gen(function* () {
57
+ const assertion = yield* makeAssertion({
58
+ clientEmail: config.clientEmail,
59
+ privateKey: Redacted.value(config.privateKey),
60
+ nowMillis: yield* Clock.currentTimeMillis
61
+ });
62
+ const response = yield* client.execute(HttpClientRequest.post(tokenUrl).pipe(HttpClientRequest.acceptJson, HttpClientRequest.bodyUrlParams({
63
+ grant_type: "urn:ietf:params:oauth:grant-type:jwt-bearer",
64
+ assertion
65
+ }))).pipe(Effect.mapError((cause) => new ConnectorError({
66
+ message: "Google OAuth token request failed",
67
+ cause
68
+ })));
69
+ const body = yield* HttpClientResponse.matchStatus(response, {
70
+ "2xx": (ok) => HttpClientResponse.schemaBodyJson(TokenResponseSchema)(ok).pipe(Effect.mapError((cause) => new ConnectorError({
71
+ message: "Invalid Google OAuth token response",
72
+ cause
73
+ }))),
74
+ orElse: tokenError
75
+ });
76
+ return {
77
+ value: Redacted.make(body.access_token),
78
+ expiresIn: Duration.seconds(body.expires_in)
79
+ };
80
+ }).pipe(Effect.tapError((error) => Telemetry.annotateError("api_auth", error)), Effect.withSpan(Telemetry.SpanName.apiFetch, {
81
+ kind: "client",
82
+ attributes: { [Telemetry.Attr.apiPath]: "/token" }
83
+ })) });
84
+ });
85
+ const layer$2 = (config) => Layer.effect(GoogleAnalyticsAuth)(make$2(config));
86
+ //#endregion
87
+ //#region src/schemas/api.ts
88
+ const ValueSchema = Schema.Struct({ value: Schema.String });
89
+ /** `POST properties/{id}:runReport`. Every value comes as a string. */
90
+ const RunReportResponseSchema = Schema.Struct({
91
+ dimensionHeaders: Schema.Array(Schema.Struct({ name: Schema.String })),
92
+ metricHeaders: Schema.Array(Schema.Struct({ name: Schema.String })),
93
+ rows: Schema.optional(Schema.Array(Schema.Struct({
94
+ dimensionValues: Schema.Array(ValueSchema),
95
+ metricValues: Schema.Array(ValueSchema)
96
+ }))),
97
+ rowCount: Schema.optional(Schema.Int),
98
+ metadata: Schema.Struct({
99
+ timeZone: Schema.String,
100
+ currencyCode: Schema.optional(Schema.String),
101
+ dataLossFromOtherRow: Schema.optional(Schema.Boolean),
102
+ subjectToThresholding: Schema.optional(Schema.Boolean),
103
+ samplingMetadatas: Schema.optional(Schema.Array(Schema.Unknown)),
104
+ dataTruncationReasons: Schema.optional(Schema.Array(Schema.Unknown))
105
+ })
106
+ });
107
+ const ErrorBodySchema = Schema.Struct({ error: Schema.Struct({
108
+ code: Schema.optional(Schema.Int),
109
+ message: Schema.optional(Schema.String),
110
+ status: Schema.optional(Schema.String)
111
+ }) });
112
+ //#endregion
113
+ //#region src/client/client.ts
114
+ var client_exports = /* @__PURE__ */ __exportAll({
115
+ DATA_API_VERSION: () => DATA_API_VERSION,
116
+ GoogleAnalyticsClient: () => GoogleAnalyticsClient,
117
+ layer: () => layer$1,
118
+ layerConfig: () => layerConfig$1,
119
+ make: () => make$1
120
+ });
121
+ const DATA_API_VERSION = "v1beta";
122
+ const baseUrl = `https://analyticsdata.googleapis.com/${DATA_API_VERSION}`;
123
+ const maxRetries = 5;
124
+ const retryBaseDelay = Duration.seconds(1);
125
+ const requestTimeout = Duration.minutes(2);
126
+ const limitRetry = Schedule.min([Schedule.exponential("1 minute"), Schedule.spaced("10 minutes")]);
127
+ const maxConcurrent = 5;
128
+ var GoogleAnalyticsClient = class extends Context.Service()("@useairfoil/producer-google-analytics/GoogleAnalyticsClient") {};
129
+ var QuotaExhausted = class extends Data.TaggedError("QuotaExhausted") {};
130
+ const statusError = (response, name) => HttpClientResponse.schemaBodyJson(ErrorBodySchema)(response).pipe(Effect.map((body) => body.error), Effect.orElseSucceed(() => void 0), Effect.flatMap((error) => response.status === 429 ? Effect.fail(new QuotaExhausted({ message: `Google Analytics quota used up for ${name}${error?.message ? `: ${error.message}` : ""}` })) : Effect.fail(new ConnectorError({ message: `Google Analytics API returned ${response.status}${error?.status ? ` ${error.status}` : ""} for ${name}${error?.message ? `: ${error.message}` : ""}` }))), Effect.tapError((error) => Telemetry.annotateError("api_status", error)));
131
+ const make$1 = Effect.fnUntraced(function* (config) {
132
+ const auth = yield* GoogleAnalyticsAuth;
133
+ const semaphore = yield* Semaphore.make(maxConcurrent);
134
+ const client = (yield* HttpClient.HttpClient).pipe(HttpClient.mapRequest(HttpClientRequest.acceptJson), HttpClient.mapRequestEffect((request) => auth.get.pipe(Effect.map((token) => HttpClientRequest.bearerToken(request, Redacted.value(token))))), HttpClient.retryTransient({ schedule: Metrics.retrySchedule({
135
+ connector: manifest.name,
136
+ baseDelay: retryBaseDelay,
137
+ times: maxRetries
138
+ }) }));
139
+ const url = `${baseUrl}/properties/${config.propertyId}:runReport`;
140
+ const runReport = (name, request) => Effect.gen(function* () {
141
+ const response = yield* client.execute(HttpClientRequest.post(url).pipe(HttpClientRequest.bodyJsonUnsafe({
142
+ dateRanges: [{
143
+ startDate: request.startDate,
144
+ endDate: request.endDate
145
+ }],
146
+ dimensions: request.dimensions.map((dimension) => ({ name: dimension })),
147
+ metrics: request.metrics.map((metric) => ({ name: metric })),
148
+ orderBys: request.dimensions.map((dimension) => ({ dimension: { dimensionName: dimension } })),
149
+ limit: request.limit,
150
+ offset: request.offset
151
+ }))).pipe(Effect.tapError((error) => Telemetry.annotateError("api_http", error)), Effect.mapError((error) => new ConnectorError({
152
+ message: `Google Analytics API request failed for ${name}`,
153
+ cause: error
154
+ })));
155
+ return yield* HttpClientResponse.matchStatus(response, {
156
+ "2xx": (ok) => HttpClientResponse.schemaBodyJson(RunReportResponseSchema)(ok).pipe(Effect.tapError((error) => Telemetry.annotateError("api_decode", error)), Effect.mapError((error) => new ConnectorError({
157
+ message: `Google Analytics API response for ${name} does not match the schema`,
158
+ cause: error
159
+ }))),
160
+ orElse: (failed) => statusError(failed, name)
161
+ });
162
+ }).pipe(Effect.timeoutOrElse({
163
+ duration: requestTimeout,
164
+ orElse: () => Effect.fail(new ConnectorError({ message: `Google Analytics API request timed out for ${name}` }))
165
+ }), semaphore.withPermits(1), Effect.tapErrorTag("QuotaExhausted", (error) => Effect.logWarning(`${error.message}. Waiting to try again.`)), Effect.retry({
166
+ schedule: limitRetry,
167
+ while: (error) => error._tag === "QuotaExhausted"
168
+ }), Effect.catchTag("QuotaExhausted", (error) => Effect.fail(new ConnectorError({ message: error.message }))), Effect.withSpan(Telemetry.SpanName.apiFetch, {
169
+ kind: "client",
170
+ attributes: { [Telemetry.Attr.apiPath]: `runReport:${name}` }
171
+ }));
172
+ const timeZone = yield* Effect.cachedWithTTL(runReport("time_zone", {
173
+ dimensions: ["date"],
174
+ metrics: ["sessions"],
175
+ startDate: "yesterday",
176
+ endDate: "yesterday",
177
+ limit: 1,
178
+ offset: 0
179
+ }).pipe(Effect.flatMap((response) => DateTime.zoneMakeNamedEffect(response.metadata.timeZone).pipe(Effect.mapError((cause) => new ConnectorError({
180
+ message: `Unknown Google Analytics time zone ${response.metadata.timeZone}`,
181
+ cause
182
+ }))))), (exit) => Exit.isSuccess(exit) ? Duration.hours(1) : Duration.zero);
183
+ return GoogleAnalyticsClient.of({
184
+ runReport,
185
+ timeZone
186
+ });
187
+ });
188
+ const layer$1 = (config) => Layer.effect(GoogleAnalyticsClient)(make$1(config)).pipe(Layer.provide(layer$2(config)));
189
+ const layerConfig$1 = (config) => Layer.unwrap(Config.unwrap(config).pipe(Effect.map(layer$1)));
190
+ //#endregion
191
+ //#region src/schemas/shared.ts
192
+ const field = (fieldId, description) => Iceberg.field(fieldId, { description });
193
+ /** The API's `YYYYMMDD` as `YYYY-MM-DD`. */
194
+ const isoDay = (date) => `${date.slice(0, 4)}-${date.slice(4, 6)}-${date.slice(6)}`;
195
+ const ReportDate = Schema.String.check(Schema.isPattern(/^\d{8}$/)).pipe(Schema.decodeTo(Schema.String, SchemaTransformation.transform({
196
+ decode: isoDay,
197
+ encode: (date) => date.replaceAll("-", "")
198
+ })));
199
+ /** `TYPE_INTEGER` metrics, as an Iceberg long. */
200
+ const count = (fieldId, description) => Schema.BigIntFromString.pipe(field(fieldId, description));
201
+ /** Float, currency, and seconds metrics, as an Iceberg double. */
202
+ const amount = (fieldId, description) => Schema.FiniteFromString.pipe(field(fieldId, description));
203
+ /** Dimension values are never null. GA uses `(not set)` and `(other)` instead. */
204
+ const dimension = (fieldId, description) => Schema.String.pipe(field(fieldId, description));
205
+ /** `version` is the fetch time, since report rows have no update time. */
206
+ const common = {
207
+ id: Schema.String.pipe(field(1, "JSON array of the property ID, date, and dimension values.")),
208
+ version: Schema.Date.pipe(field(2, "Time the row was read from Google Analytics.")),
209
+ property_id: Schema.String.pipe(field(3, "GA4 property ID.")),
210
+ date: ReportDate.pipe(field(4, "Day in the property's time zone, as YYYY-MM-DD."))
211
+ };
212
+ const currencyCode = Schema.String.pipe(field(50, "Currency of the revenue fields, the property's currency."));
213
+ //#endregion
214
+ //#region src/resources/reports/read.ts
215
+ const pageSize = 1e5;
216
+ /** `[column, apiName]` pairs, in request order. */
217
+ const pairs = (columns) => Object.entries(columns).flatMap(([column, name]) => name === void 0 ? [] : [[column, name]]);
218
+ const valuesByName = (headers, values) => new Map(headers.map((header, index) => [header.name, values[index]?.value]));
219
+ /** Logs the response flags that mean some data is hidden or estimated. */
220
+ const warnDataLoss = (name, day, metadata) => {
221
+ const flags = {
222
+ otherRow: metadata.dataLossFromOtherRow === true,
223
+ thresholded: metadata.subjectToThresholding === true,
224
+ sampled: (metadata.samplingMetadatas?.length ?? 0) > 0,
225
+ truncated: (metadata.dataTruncationReasons?.length ?? 0) > 0
226
+ };
227
+ return Object.values(flags).some(Boolean) ? Effect.logWarning("Google Analytics report is incomplete").pipe(Effect.annotateLogs({
228
+ report: name,
229
+ day,
230
+ ...flags
231
+ })) : Effect.void;
232
+ };
233
+ const readDay = (client, propertyId, spec, options) => Effect.gen(function* () {
234
+ const dimensions = pairs(spec.dimensions);
235
+ const metrics = pairs(spec.metrics);
236
+ const columns = [...dimensions, ...metrics];
237
+ const response = yield* client.runReport(spec.name, {
238
+ dimensions: ["date", ...dimensions.map(([, name]) => name)],
239
+ metrics: metrics.map(([, name]) => name),
240
+ startDate: options.day,
241
+ endDate: options.day,
242
+ limit: options.limit ?? pageSize,
243
+ offset: options.offset
244
+ });
245
+ yield* warnDataLoss(spec.name, options.day, response.metadata);
246
+ const version = yield* DateTime.nowAsDate;
247
+ const decodeRow = Schema.decodeUnknownEffect(spec.rowSchema);
248
+ const toRow = (row) => {
249
+ const values = new Map([...valuesByName(response.dimensionHeaders, row.dimensionValues), ...valuesByName(response.metricHeaders, row.metricValues)]);
250
+ const date = values.get("date") ?? "";
251
+ return decodeRow({
252
+ id: JSON.stringify([
253
+ propertyId,
254
+ isoDay(date),
255
+ ...dimensions.map(([, name]) => values.get(name))
256
+ ]),
257
+ version,
258
+ property_id: propertyId,
259
+ date,
260
+ currency_code: response.metadata.currencyCode,
261
+ ...Object.fromEntries(columns.map(([column, name]) => [column, values.get(name)]))
262
+ }).pipe(Effect.mapError((cause) => new ConnectorError({
263
+ message: `Unexpected Google Analytics row in ${spec.name} for ${options.day}`,
264
+ cause
265
+ })));
266
+ };
267
+ const rows = yield* Effect.forEach(response.rows ?? [], toRow);
268
+ const read = options.offset + rows.length;
269
+ return {
270
+ rows,
271
+ next: rows.length > 0 && read < (response.rowCount ?? 0) ? Option.some(read) : Option.none()
272
+ };
273
+ });
274
+ //#endregion
275
+ //#region src/resources/reports/sources.ts
276
+ const changesInterval = Duration.hours(6);
277
+ const Day = Schema.String.check(Schema.isPattern(/^\d{4}-\d{2}-\d{2}$/));
278
+ const addDays = (day, days) => DateTime.formatIsoDateUtc(DateTime.add(DateTime.makeUnsafe(day), { days }));
279
+ /** The calendar day of `time` in the property's time zone. */
280
+ const dayIn = (zone, time) => DateTime.formatIsoDate(DateTime.setZone(time, zone));
281
+ const minDay = (a, b) => a < b ? a : b;
282
+ /** Reports cover whole days, so backfill ends the day before the cutoff. */
283
+ const lastBackfillDay = (zone, cutoff) => addDays(dayIn(zone, cutoff), -1);
284
+ const firstDay = "2015-08-14";
285
+ const startDay = (config, end) => Option.match(config.startDate, {
286
+ onNone: () => Effect.succeed(DateTime.formatIsoDateUtc(DateTime.add(DateTime.makeUnsafe(end), { years: -2 }))),
287
+ onSome: (startDate) => Schema.decodeUnknownEffect(Day)(startDate).pipe(Effect.mapError((cause) => new ConnectorError({
288
+ message: "GA_START_DATE must be YYYY-MM-DD",
289
+ cause
290
+ })))
291
+ }).pipe(Effect.map((day) => day < firstDay ? firstDay : day));
292
+ const cursorError = (cause) => new ConnectorError({
293
+ message: "Invalid Google Analytics cursor",
294
+ cause
295
+ });
296
+ const PageCursorSchema = Schema.fromJsonString(Schema.Struct({
297
+ day: Day,
298
+ offset: Schema.Int
299
+ }));
300
+ /**
301
+ * `through` is the last day fully read. `pending` is set while a run is split:
302
+ * the day and offset to go on from, and the last day of that run.
303
+ */
304
+ const ChangesCursorSchema = Schema.Struct({
305
+ through: Day,
306
+ pending: Schema.optional(Schema.Struct({
307
+ day: Day,
308
+ end: Day,
309
+ offset: Schema.Int
310
+ }))
311
+ });
312
+ const ChangesCursorCodec = Schema.Union([Schema.fromJsonString(ChangesCursorSchema), Schema.DateTimeUtcFromString]);
313
+ const reportCheck = (client, config, spec) => Effect.gen(function* () {
314
+ const zone = yield* client.timeZone;
315
+ const yesterday = addDays(dayIn(zone, yield* DateTime.now), -1);
316
+ yield* startDay(config, yesterday);
317
+ yield* readDay(client, config.propertyId, spec, {
318
+ day: yesterday,
319
+ offset: 0,
320
+ limit: 1
321
+ });
322
+ });
323
+ const reportBackfill = (client, config, spec) => Fetch.page({
324
+ pageCursor: Cursor.string(),
325
+ cutoff: Cursor.isoDateTime(),
326
+ fetch: ({ pageCursor, cutoff }) => Effect.gen(function* () {
327
+ const zone = yield* client.timeZone;
328
+ const cutoffTime = yield* Schema.decodeUnknownEffect(Schema.DateTimeUtcFromString)(String(cutoff)).pipe(Effect.mapError(cursorError));
329
+ const end = lastBackfillDay(zone, cutoffTime);
330
+ const at = pageCursor === void 0 ? {
331
+ day: yield* startDay(config, end),
332
+ offset: 0
333
+ } : yield* Schema.decodeUnknownEffect(PageCursorSchema)(String(pageCursor)).pipe(Effect.mapError(cursorError));
334
+ if (at.day > end) return {
335
+ rows: [],
336
+ hasMore: false
337
+ };
338
+ const page = yield* readDay(client, config.propertyId, spec, at);
339
+ const next = Option.match(page.next, {
340
+ onSome: (offset) => ({
341
+ day: at.day,
342
+ offset
343
+ }),
344
+ onNone: () => ({
345
+ day: addDays(at.day, 1),
346
+ offset: 0
347
+ })
348
+ });
349
+ if (next.day > end) return {
350
+ rows: page.rows,
351
+ hasMore: false
352
+ };
353
+ return {
354
+ rows: page.rows,
355
+ hasMore: true,
356
+ nextPageCursor: yield* Schema.encodeEffect(PageCursorSchema)(next).pipe(Effect.mapError(cursorError))
357
+ };
358
+ })
359
+ });
360
+ /**
361
+ * Reads the last `lookbackDays` again on every run, since Google keeps
362
+ * updating recent days. After downtime it also reads the days it missed.
363
+ */
364
+ const reportChanges = (client, config, spec) => Fetch.changes({
365
+ cursor: Cursor.string(),
366
+ interval: changesInterval,
367
+ fetch: ({ cursor }) => Effect.gen(function* () {
368
+ const zone = yield* client.timeZone;
369
+ const decoded = yield* Schema.decodeUnknownEffect(ChangesCursorCodec)(String(cursor)).pipe(Effect.mapError(cursorError));
370
+ const state = DateTime.isDateTime(decoded) ? { through: lastBackfillDay(zone, decoded) } : decoded;
371
+ const yesterday = addDays(dayIn(zone, yield* DateTime.now), -1);
372
+ const window = state.pending ?? {
373
+ day: minDay(addDays(state.through, 1), addDays(yesterday, 1 - config.lookbackDays)),
374
+ end: yesterday,
375
+ offset: 0
376
+ };
377
+ const page = yield* readDay(client, config.propertyId, spec, window);
378
+ const pending = Option.match(page.next, {
379
+ onSome: (offset) => Option.some({
380
+ ...window,
381
+ offset
382
+ }),
383
+ onNone: () => window.day < window.end ? Option.some({
384
+ ...window,
385
+ day: addDays(window.day, 1),
386
+ offset: 0
387
+ }) : Option.none()
388
+ });
389
+ const next = Option.match(pending, {
390
+ onSome: (pending) => ({
391
+ through: state.through,
392
+ pending
393
+ }),
394
+ onNone: () => ({ through: window.end })
395
+ });
396
+ return {
397
+ rows: page.rows,
398
+ cursor: yield* Schema.encodeEffect(Schema.fromJsonString(ChangesCursorSchema))(next).pipe(Effect.mapError(cursorError)),
399
+ hasMore: Option.isSome(pending)
400
+ };
401
+ })
402
+ });
403
+ //#endregion
404
+ //#region src/resources/reports/resource.ts
405
+ const reportEntity = (client, config, spec) => ({
406
+ name: spec.name,
407
+ rowSchema: spec.rowSchema,
408
+ version: "version",
409
+ check: reportCheck(client, config, spec),
410
+ backfill: reportBackfill(client, config, spec),
411
+ changes: reportChanges(client, config, spec)
412
+ });
413
+ //#endregion
414
+ //#region src/schemas/reports.ts
415
+ const DailyOverviewSchema = Schema.Struct({
416
+ ...common,
417
+ active_users: count(30, "Users who engaged on the day."),
418
+ new_users: count(31, "Users who came for the first time."),
419
+ total_users: count(32, "Users who logged any event."),
420
+ sessions: count(33, "Sessions that started."),
421
+ engaged_sessions: count(34, "Sessions that lasted 10 seconds or more, or had a key event or 2 page views."),
422
+ screen_page_views: count(35, "Page and screen views."),
423
+ event_count: count(36, "Events."),
424
+ key_events: amount(37, "Key events. Can be fractional after attribution."),
425
+ total_revenue: amount(38, "Revenue from purchases, subscriptions, and ads."),
426
+ user_engagement_duration: amount(39, "Seconds the site or app was in the foreground."),
427
+ currency_code: currencyCode
428
+ }).annotate({ description: "Google Analytics totals per day." }).pipe(Iceberg.identifier("id"));
429
+ const TrafficSourceSchema = Schema.Struct({
430
+ ...common,
431
+ session_source: dimension(10, "Source of the session, such as google."),
432
+ session_medium: dimension(11, "Medium of the session, such as organic."),
433
+ session_campaign_name: dimension(12, "Campaign of the session."),
434
+ session_default_channel_group: dimension(13, "Default channel group of the session."),
435
+ sessions: count(30, "Sessions that started."),
436
+ engaged_sessions: count(31, "Engaged sessions."),
437
+ active_users: count(32, "Users who engaged."),
438
+ new_users: count(33, "Users who came for the first time."),
439
+ screen_page_views: count(34, "Page and screen views."),
440
+ event_count: count(35, "Events."),
441
+ key_events: amount(36, "Key events."),
442
+ total_revenue: amount(37, "Revenue from purchases, subscriptions, and ads."),
443
+ user_engagement_duration: amount(38, "Seconds the site or app was in the foreground."),
444
+ currency_code: currencyCode
445
+ }).annotate({ description: "Google Analytics sessions per day and traffic source." }).pipe(Iceberg.identifier("id"));
446
+ const UserAcquisitionSchema = Schema.Struct({
447
+ ...common,
448
+ first_user_source: dimension(10, "Source that first brought the user."),
449
+ first_user_medium: dimension(11, "Medium that first brought the user."),
450
+ first_user_campaign_name: dimension(12, "Campaign that first brought the user."),
451
+ first_user_default_channel_group: dimension(13, "Default channel group that first brought the user."),
452
+ new_users: count(30, "Users who came for the first time."),
453
+ active_users: count(31, "Users who engaged."),
454
+ sessions: count(32, "Sessions that started."),
455
+ engaged_sessions: count(33, "Engaged sessions."),
456
+ key_events: amount(34, "Key events."),
457
+ total_revenue: amount(35, "Revenue from purchases, subscriptions, and ads."),
458
+ currency_code: currencyCode
459
+ }).annotate({ description: "Google Analytics users per day and first traffic source." }).pipe(Iceberg.identifier("id"));
460
+ const PageSchema = Schema.Struct({
461
+ ...common,
462
+ host_name: dimension(10, "Host name, such as www.example.com."),
463
+ page_path: dimension(11, "Page path, without the query string."),
464
+ page_title: dimension(12, "Page title."),
465
+ screen_page_views: count(30, "Page and screen views."),
466
+ active_users: count(31, "Users who engaged."),
467
+ event_count: count(32, "Events."),
468
+ key_events: amount(33, "Key events."),
469
+ user_engagement_duration: amount(34, "Seconds the page was in the foreground.")
470
+ }).annotate({ description: "Google Analytics page views per day and page." }).pipe(Iceberg.identifier("id"));
471
+ const LandingPageSchema = Schema.Struct({
472
+ ...common,
473
+ landing_page: dimension(10, "First page path of the session, without the query string."),
474
+ sessions: count(30, "Sessions that started."),
475
+ engaged_sessions: count(31, "Engaged sessions."),
476
+ active_users: count(32, "Users who engaged."),
477
+ new_users: count(33, "Users who came for the first time."),
478
+ key_events: amount(34, "Key events."),
479
+ total_revenue: amount(35, "Revenue from purchases, subscriptions, and ads."),
480
+ currency_code: currencyCode
481
+ }).annotate({ description: "Google Analytics sessions per day and landing page." }).pipe(Iceberg.identifier("id"));
482
+ const EventSchema = Schema.Struct({
483
+ ...common,
484
+ event_name: dimension(10, "Event name, such as page_view."),
485
+ event_count: count(30, "Events."),
486
+ total_users: count(31, "Users who logged the event."),
487
+ event_value: amount(32, "Sum of the event's value parameter."),
488
+ key_events: amount(33, "Key events.")
489
+ }).annotate({ description: "Google Analytics events per day and event name." }).pipe(Iceberg.identifier("id"));
490
+ const LocationSchema = Schema.Struct({
491
+ ...common,
492
+ country: dimension(10, "Country of the user."),
493
+ region: dimension(11, "Region of the user."),
494
+ city: dimension(12, "City of the user."),
495
+ active_users: count(30, "Users who engaged."),
496
+ new_users: count(31, "Users who came for the first time."),
497
+ sessions: count(32, "Sessions that started."),
498
+ engaged_sessions: count(33, "Engaged sessions."),
499
+ key_events: amount(34, "Key events."),
500
+ total_revenue: amount(35, "Revenue from purchases, subscriptions, and ads."),
501
+ currency_code: currencyCode
502
+ }).annotate({ description: "Google Analytics users per day and location." }).pipe(Iceberg.identifier("id"));
503
+ const DeviceSchema = Schema.Struct({
504
+ ...common,
505
+ device_category: dimension(10, "Device category: desktop, mobile, or tablet."),
506
+ platform: dimension(11, "Platform: web, iOS, or Android."),
507
+ operating_system: dimension(12, "Operating system."),
508
+ browser: dimension(13, "Browser."),
509
+ active_users: count(30, "Users who engaged."),
510
+ new_users: count(31, "Users who came for the first time."),
511
+ sessions: count(32, "Sessions that started."),
512
+ engaged_sessions: count(33, "Engaged sessions."),
513
+ screen_page_views: count(34, "Page and screen views."),
514
+ key_events: amount(35, "Key events.")
515
+ }).annotate({ description: "Google Analytics users per day and device." }).pipe(Iceberg.identifier("id"));
516
+ const EcommerceItemSchema = Schema.Struct({
517
+ ...common,
518
+ item_id: dimension(10, "Item ID."),
519
+ item_name: dimension(11, "Item name."),
520
+ item_brand: dimension(12, "Item brand."),
521
+ item_category: dimension(13, "First item category."),
522
+ items_viewed: count(30, "Units viewed."),
523
+ items_added_to_cart: count(31, "Units added to carts."),
524
+ items_purchased: count(32, "Units purchased."),
525
+ item_revenue: amount(33, "Revenue from the item, without tax and shipping."),
526
+ currency_code: currencyCode
527
+ }).annotate({ description: "Google Analytics ecommerce items per day. Empty without ecommerce events." }).pipe(Iceberg.identifier("id"));
528
+ //#endregion
529
+ //#region src/resources/reports/specs.ts
530
+ const dailyOverview = {
531
+ name: "daily_overview",
532
+ dimensions: {},
533
+ metrics: {
534
+ active_users: "activeUsers",
535
+ new_users: "newUsers",
536
+ total_users: "totalUsers",
537
+ sessions: "sessions",
538
+ engaged_sessions: "engagedSessions",
539
+ screen_page_views: "screenPageViews",
540
+ event_count: "eventCount",
541
+ key_events: "keyEvents",
542
+ total_revenue: "totalRevenue",
543
+ user_engagement_duration: "userEngagementDuration"
544
+ },
545
+ rowSchema: DailyOverviewSchema
546
+ };
547
+ const trafficSources = {
548
+ name: "traffic_sources",
549
+ dimensions: {
550
+ session_source: "sessionSource",
551
+ session_medium: "sessionMedium",
552
+ session_campaign_name: "sessionCampaignName",
553
+ session_default_channel_group: "sessionDefaultChannelGroup"
554
+ },
555
+ metrics: {
556
+ sessions: "sessions",
557
+ engaged_sessions: "engagedSessions",
558
+ active_users: "activeUsers",
559
+ new_users: "newUsers",
560
+ screen_page_views: "screenPageViews",
561
+ event_count: "eventCount",
562
+ key_events: "keyEvents",
563
+ total_revenue: "totalRevenue",
564
+ user_engagement_duration: "userEngagementDuration"
565
+ },
566
+ rowSchema: TrafficSourceSchema
567
+ };
568
+ const userAcquisition = {
569
+ name: "user_acquisition",
570
+ dimensions: {
571
+ first_user_source: "firstUserSource",
572
+ first_user_medium: "firstUserMedium",
573
+ first_user_campaign_name: "firstUserCampaignName",
574
+ first_user_default_channel_group: "firstUserDefaultChannelGroup"
575
+ },
576
+ metrics: {
577
+ new_users: "newUsers",
578
+ active_users: "activeUsers",
579
+ sessions: "sessions",
580
+ engaged_sessions: "engagedSessions",
581
+ key_events: "keyEvents",
582
+ total_revenue: "totalRevenue"
583
+ },
584
+ rowSchema: UserAcquisitionSchema
585
+ };
586
+ const pages = {
587
+ name: "pages",
588
+ dimensions: {
589
+ host_name: "hostName",
590
+ page_path: "pagePath",
591
+ page_title: "pageTitle"
592
+ },
593
+ metrics: {
594
+ screen_page_views: "screenPageViews",
595
+ active_users: "activeUsers",
596
+ event_count: "eventCount",
597
+ key_events: "keyEvents",
598
+ user_engagement_duration: "userEngagementDuration"
599
+ },
600
+ rowSchema: PageSchema
601
+ };
602
+ const landingPages = {
603
+ name: "landing_pages",
604
+ dimensions: { landing_page: "landingPage" },
605
+ metrics: {
606
+ sessions: "sessions",
607
+ engaged_sessions: "engagedSessions",
608
+ active_users: "activeUsers",
609
+ new_users: "newUsers",
610
+ key_events: "keyEvents",
611
+ total_revenue: "totalRevenue"
612
+ },
613
+ rowSchema: LandingPageSchema
614
+ };
615
+ const events = {
616
+ name: "events",
617
+ dimensions: { event_name: "eventName" },
618
+ metrics: {
619
+ event_count: "eventCount",
620
+ total_users: "totalUsers",
621
+ event_value: "eventValue",
622
+ key_events: "keyEvents"
623
+ },
624
+ rowSchema: EventSchema
625
+ };
626
+ const locations = {
627
+ name: "locations",
628
+ dimensions: {
629
+ country: "country",
630
+ region: "region",
631
+ city: "city"
632
+ },
633
+ metrics: {
634
+ active_users: "activeUsers",
635
+ new_users: "newUsers",
636
+ sessions: "sessions",
637
+ engaged_sessions: "engagedSessions",
638
+ key_events: "keyEvents",
639
+ total_revenue: "totalRevenue"
640
+ },
641
+ rowSchema: LocationSchema
642
+ };
643
+ const devices = {
644
+ name: "devices",
645
+ dimensions: {
646
+ device_category: "deviceCategory",
647
+ platform: "platform",
648
+ operating_system: "operatingSystem",
649
+ browser: "browser"
650
+ },
651
+ metrics: {
652
+ active_users: "activeUsers",
653
+ new_users: "newUsers",
654
+ sessions: "sessions",
655
+ engaged_sessions: "engagedSessions",
656
+ screen_page_views: "screenPageViews",
657
+ key_events: "keyEvents"
658
+ },
659
+ rowSchema: DeviceSchema
660
+ };
661
+ const ecommerceItems = {
662
+ name: "ecommerce_items",
663
+ dimensions: {
664
+ item_id: "itemId",
665
+ item_name: "itemName",
666
+ item_brand: "itemBrand",
667
+ item_category: "itemCategory"
668
+ },
669
+ metrics: {
670
+ items_viewed: "itemsViewed",
671
+ items_added_to_cart: "itemsAddedToCart",
672
+ items_purchased: "itemsPurchased",
673
+ item_revenue: "itemRevenue"
674
+ },
675
+ rowSchema: EcommerceItemSchema
676
+ };
677
+ //#endregion
678
+ //#region src/connector.ts
679
+ var connector_exports = /* @__PURE__ */ __exportAll({
680
+ GoogleAnalyticsConfigDef: () => GoogleAnalyticsConfigDef,
681
+ GoogleAnalyticsConnector: () => GoogleAnalyticsConnector,
682
+ layer: () => layer,
683
+ layerConfig: () => layerConfig,
684
+ make: () => make,
685
+ manifest: () => manifest
686
+ });
687
+ const make = Effect.fnUntraced(function* (config) {
688
+ const client = yield* GoogleAnalyticsClient;
689
+ return Connector.define({
690
+ name: "producer-google-analytics",
691
+ title: "Google Analytics",
692
+ resources: [
693
+ Resource.entity(reportEntity(client, config, dailyOverview)),
694
+ Resource.entity(reportEntity(client, config, trafficSources)),
695
+ Resource.entity(reportEntity(client, config, userAcquisition)),
696
+ Resource.entity(reportEntity(client, config, pages)),
697
+ Resource.entity(reportEntity(client, config, landingPages)),
698
+ Resource.entity(reportEntity(client, config, events)),
699
+ Resource.entity(reportEntity(client, config, locations)),
700
+ Resource.entity(reportEntity(client, config, devices)),
701
+ Resource.entity(reportEntity(client, config, ecommerceItems))
702
+ ]
703
+ });
704
+ });
705
+ var GoogleAnalyticsConnector = class extends Context.Service()("@useairfoil/producer-google-analytics/GoogleAnalyticsConnector") {};
706
+ const layer = (config) => Layer.effect(GoogleAnalyticsConnector)(make(config).pipe(Effect.annotateLogs({ component: "google-analytics" }))).pipe(Layer.provide(layer$1(config)));
707
+ const layerConfig = (config) => Layer.unwrap(Config.unwrap(config).pipe(Effect.map(layer)));
708
+ //#endregion
709
+ export { DeviceSchema as a, LandingPageSchema as c, TrafficSourceSchema as d, UserAcquisitionSchema as f, DailyOverviewSchema as i, LocationSchema as l, auth_exports as m, connector_exports as n, EcommerceItemSchema as o, client_exports as p, layerConfig as r, EventSchema as s, GoogleAnalyticsConnector as t, PageSchema as u };
710
+
711
+ //# sourceMappingURL=src-Cu0LuOTi.mjs.map