@useairfoil/producer-google-analytics 0.0.0-stage → 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.
- package/README.md +74 -2
- package/dist/index.d.mts +773 -0
- package/dist/index.mjs +3 -0
- package/dist/main.d.mts +1 -0
- package/dist/main.mjs +662 -0
- package/dist/main.mjs.map +1 -0
- package/dist/manifest.d.mts +45 -0
- package/dist/manifest.mjs +75 -0
- package/dist/manifest.mjs.map +1 -0
- package/dist/rolldown-runtime-D7D4PA-g.mjs +13 -0
- package/dist/src-Cu0LuOTi.mjs +711 -0
- package/dist/src-Cu0LuOTi.mjs.map +1 -0
- package/package.json +48 -4
- package/src/client/auth.ts +133 -0
- package/src/client/client.ts +212 -0
- package/src/connector.ts +55 -0
- package/src/create-table.ts +5 -0
- package/src/demo/index.ts +44 -0
- package/src/demo/reports.ts +251 -0
- package/src/demo/shared.ts +98 -0
- package/src/index.ts +26 -0
- package/src/main.ts +22 -0
- package/src/manifest.ts +50 -0
- package/src/resources/reports/read.ts +104 -0
- package/src/resources/reports/resource.ts +31 -0
- package/src/resources/reports/sources.ts +169 -0
- package/src/resources/reports/spec.ts +15 -0
- package/src/resources/reports/specs.ts +173 -0
- package/src/sandbox.ts +37 -0
- package/src/schemas/api.ts +37 -0
- package/src/schemas/reports.ts +172 -0
- package/src/schemas/shared.ts +42 -0
- package/src/start.ts +49 -0
- package/src/tables.ts +25 -0
|
@@ -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
|