@memberjunction/connector-ga4 0.2.0

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,72 @@
1
+ /**
2
+ * `CompanyIntegration.Configuration` for GA4 — the non-secret half of the setup.
3
+ *
4
+ * The secret half (the service-account key) lives in `MJ: Credentials` and is never read from here.
5
+ * Everything in this file is safe to log, which is why the property id is here rather than in the
6
+ * credential: "which property am I pointed at" is the first question any support conversation asks.
7
+ *
8
+ * Every field except `propertyId` is optional, and every parse failure falls back to the documented
9
+ * default rather than throwing. A malformed `lookbackDays` should not take a working integration
10
+ * offline; a missing `propertyId` genuinely must, because there is nothing to query.
11
+ */
12
+ export interface GA4Config {
13
+ /** The numeric GA4 property id, as a string. Digits only — GA4 rejects anything else. */
14
+ propertyId: string;
15
+ /** Days of already-synced history to re-read on every incremental run. See {@link DEFAULT_LOOKBACK_DAYS}. */
16
+ lookbackDays: number;
17
+ /** Earliest date to read when there is no watermark, `YYYY-MM-DD`. */
18
+ startDate: string | null;
19
+ /** Maximum span, in days, of any single GA4 request. See {@link DEFAULT_MAX_WINDOW_DAYS}. */
20
+ maxWindowDays: number;
21
+ }
22
+ /**
23
+ * Three days.
24
+ *
25
+ * GA4 does not finalize a day when the day ends. Event data continues to arrive and be reprocessed
26
+ * for up to 48 hours, and user-scoped metrics — the two cardinalities — can move for longer as late
27
+ * sessions are stitched onto existing users. A watermark that advanced strictly to the last day seen
28
+ * would therefore land every day's numbers exactly once, at their least accurate, and never correct
29
+ * them.
30
+ *
31
+ * Re-reading the tail is cheap in the only way that matters: the engine's content-hash prefetch turns
32
+ * an unchanged row into zero writes, so the cost of a lookback window is reads, and the days that DID
33
+ * move are precisely the ones that should be rewritten.
34
+ *
35
+ * Three rather than two: GA4's 48 hours is measured from the event, not from midnight in the
36
+ * property's reporting time zone, so a strict two-day window can end a few hours short of it.
37
+ */
38
+ export declare const DEFAULT_LOOKBACK_DAYS = 3;
39
+ /** Sanity bound. A lookback longer than this is a request for a full re-read; clear the watermark instead. */
40
+ export declare const MAX_LOOKBACK_DAYS = 400;
41
+ /**
42
+ * Ninety days per request.
43
+ *
44
+ * This does NOT bound how much history a run covers — when a window is exhausted and there is more
45
+ * to read, the cursor advances to the next window and the same run keeps going. It bounds the size
46
+ * of any single GA4 response, which matters because a wide date range against a high-cardinality
47
+ * dimension like `pagePath` is exactly the shape that trips GA4's own cardinality limits and starts
48
+ * collapsing rows into the `(other)` bucket. Narrower windows keep each request inside those limits.
49
+ */
50
+ export declare const DEFAULT_MAX_WINDOW_DAYS = 90;
51
+ /** GA4's own hard cap on rows per `runReport` request. */
52
+ export declare const GA4_MAX_LIMIT = 250000;
53
+ /**
54
+ * How far back a cold start reads when `startDate` is not configured.
55
+ *
56
+ * Slightly over GA4's default 14-month event-data retention, so the default behaviour is "everything
57
+ * the property still has" rather than an arbitrary window. Properties set to 2-month retention will
58
+ * simply return nothing for the earlier part, at no cost beyond a few empty requests.
59
+ */
60
+ export declare const DEFAULT_COLD_START_DAYS = 430;
61
+ /**
62
+ * Parse `CompanyIntegration.Configuration`.
63
+ *
64
+ * @throws when `propertyId` is absent or is not digits — the one unrecoverable case.
65
+ */
66
+ export declare function parseGA4Config(configuration: string | null | undefined): GA4Config;
67
+ /**
68
+ * Accept the property id as a string or a number, and tolerate the two forms people paste from the
69
+ * GA4 UI and the API docs — `properties/123456789` and a stray `G-` measurement id is rejected,
70
+ * because silently querying the wrong thing is worse than failing setup.
71
+ */
72
+ export declare function normalizePropertyId(value: unknown): string | null;
@@ -0,0 +1,103 @@
1
+ /**
2
+ * `CompanyIntegration.Configuration` for GA4 — the non-secret half of the setup.
3
+ *
4
+ * The secret half (the service-account key) lives in `MJ: Credentials` and is never read from here.
5
+ * Everything in this file is safe to log, which is why the property id is here rather than in the
6
+ * credential: "which property am I pointed at" is the first question any support conversation asks.
7
+ *
8
+ * Every field except `propertyId` is optional, and every parse failure falls back to the documented
9
+ * default rather than throwing. A malformed `lookbackDays` should not take a working integration
10
+ * offline; a missing `propertyId` genuinely must, because there is nothing to query.
11
+ */
12
+ /**
13
+ * Three days.
14
+ *
15
+ * GA4 does not finalize a day when the day ends. Event data continues to arrive and be reprocessed
16
+ * for up to 48 hours, and user-scoped metrics — the two cardinalities — can move for longer as late
17
+ * sessions are stitched onto existing users. A watermark that advanced strictly to the last day seen
18
+ * would therefore land every day's numbers exactly once, at their least accurate, and never correct
19
+ * them.
20
+ *
21
+ * Re-reading the tail is cheap in the only way that matters: the engine's content-hash prefetch turns
22
+ * an unchanged row into zero writes, so the cost of a lookback window is reads, and the days that DID
23
+ * move are precisely the ones that should be rewritten.
24
+ *
25
+ * Three rather than two: GA4's 48 hours is measured from the event, not from midnight in the
26
+ * property's reporting time zone, so a strict two-day window can end a few hours short of it.
27
+ */
28
+ export const DEFAULT_LOOKBACK_DAYS = 3;
29
+ /** Sanity bound. A lookback longer than this is a request for a full re-read; clear the watermark instead. */
30
+ export const MAX_LOOKBACK_DAYS = 400;
31
+ /**
32
+ * Ninety days per request.
33
+ *
34
+ * This does NOT bound how much history a run covers — when a window is exhausted and there is more
35
+ * to read, the cursor advances to the next window and the same run keeps going. It bounds the size
36
+ * of any single GA4 response, which matters because a wide date range against a high-cardinality
37
+ * dimension like `pagePath` is exactly the shape that trips GA4's own cardinality limits and starts
38
+ * collapsing rows into the `(other)` bucket. Narrower windows keep each request inside those limits.
39
+ */
40
+ export const DEFAULT_MAX_WINDOW_DAYS = 90;
41
+ /** GA4's own hard cap on rows per `runReport` request. */
42
+ export const GA4_MAX_LIMIT = 250_000;
43
+ /**
44
+ * How far back a cold start reads when `startDate` is not configured.
45
+ *
46
+ * Slightly over GA4's default 14-month event-data retention, so the default behaviour is "everything
47
+ * the property still has" rather than an arbitrary window. Properties set to 2-month retention will
48
+ * simply return nothing for the earlier part, at no cost beyond a few empty requests.
49
+ */
50
+ export const DEFAULT_COLD_START_DAYS = 430;
51
+ const ISO_DATE = /^\d{4}-\d{2}-\d{2}$/;
52
+ /**
53
+ * Parse `CompanyIntegration.Configuration`.
54
+ *
55
+ * @throws when `propertyId` is absent or is not digits — the one unrecoverable case.
56
+ */
57
+ export function parseGA4Config(configuration) {
58
+ const raw = readJSONObject(configuration);
59
+ const propertyId = normalizePropertyId(raw.propertyId);
60
+ if (propertyId === null) {
61
+ throw new Error('GA4: Configuration.propertyId is required and must be the numeric GA4 property id, e.g. {"propertyId":"123456789"}. ' +
62
+ 'It is the number shown as "PROPERTY ID" in GA4 Admin → Property Settings — not the measurement id (G-XXXXXXX) and not the account id.');
63
+ }
64
+ return {
65
+ propertyId,
66
+ lookbackDays: clampInt(raw.lookbackDays, DEFAULT_LOOKBACK_DAYS, 0, MAX_LOOKBACK_DAYS),
67
+ startDate: ISO_DATE.test(String(raw.startDate)) ? String(raw.startDate) : null,
68
+ maxWindowDays: clampInt(raw.maxWindowDays, DEFAULT_MAX_WINDOW_DAYS, 1, 400),
69
+ };
70
+ }
71
+ /**
72
+ * Accept the property id as a string or a number, and tolerate the two forms people paste from the
73
+ * GA4 UI and the API docs — `properties/123456789` and a stray `G-` measurement id is rejected,
74
+ * because silently querying the wrong thing is worse than failing setup.
75
+ */
76
+ export function normalizePropertyId(value) {
77
+ if (typeof value === 'number' && Number.isInteger(value) && value > 0)
78
+ return String(value);
79
+ if (typeof value !== 'string')
80
+ return null;
81
+ const trimmed = value.trim().replace(/^properties\//, '');
82
+ return /^\d+$/.test(trimmed) ? trimmed : null;
83
+ }
84
+ function readJSONObject(text) {
85
+ if (!text)
86
+ return {};
87
+ try {
88
+ const parsed = JSON.parse(text);
89
+ return parsed !== null && typeof parsed === 'object' && !Array.isArray(parsed)
90
+ ? parsed
91
+ : {};
92
+ }
93
+ catch {
94
+ return {};
95
+ }
96
+ }
97
+ function clampInt(value, fallback, min, max) {
98
+ const n = typeof value === 'number' ? value : typeof value === 'string' ? Number(value) : NaN;
99
+ if (!Number.isFinite(n))
100
+ return fallback;
101
+ return Math.min(max, Math.max(min, Math.trunc(n)));
102
+ }
103
+ //# sourceMappingURL=GA4Config.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"GA4Config.js","sourceRoot":"","sources":["../src/GA4Config.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,82 @@
1
+ import { type IMetadataProvider, type UserInfo } from '@memberjunction/core';
2
+ import type { MJCompanyIntegrationEntity } from '@memberjunction/core-entities';
3
+ import { BaseIntegrationConnector, type ConnectionTestResult, type ExternalFieldSchema, type ExternalObjectSchema, type FetchBatchResult, type FetchContext, type IntegrationObjectInfo } from '@memberjunction/integration-engine';
4
+ import { type GA4ServiceAccount } from './GA4ServiceAccount.js';
5
+ import { type GA4ReportPort } from './GA4Report.js';
6
+ export declare class GA4Connector extends BaseIntegrationConnector {
7
+ get IntegrationName(): string;
8
+ /** The watermark is a date that only ever advances toward today. */
9
+ get MonotonicWatermark(): boolean;
10
+ /**
11
+ * GA4 orders a report by its own default and offers no stable seek key. Position is carried by
12
+ * the cursor's offset against a pinned date range instead — which is exact for the duration of a
13
+ * run, where a sort key would only be approximate.
14
+ */
15
+ StableOrderingKey(_objectName: string): string | null;
16
+ /** Overridable so tests drive the clock and the API without a credential or a network. */
17
+ protected Now(): Date;
18
+ protected Report(companyIntegration: MJCompanyIntegrationEntity, contextUser: UserInfo): Promise<GA4ReportPort>;
19
+ GetIntegrationObjects(): IntegrationObjectInfo[];
20
+ DiscoverObjects(_companyIntegration: MJCompanyIntegrationEntity, _contextUser: UserInfo): Promise<ExternalObjectSchema[]>;
21
+ /**
22
+ * MaxLength/Precision/Scale/IsPrimaryKey are set explicitly, and that is load-bearing. The engine
23
+ * has two bridges from a declared catalog into field schemas and they are not equivalent — one
24
+ * carries these attributes, the other drops them. A `--base` connector that leaves them to be
25
+ * inferred gets unbounded columns and hash-based identity instead of key-based.
26
+ */
27
+ DiscoverFields(_companyIntegration: MJCompanyIntegrationEntity, objectName: string, _contextUser: UserInfo): Promise<ExternalFieldSchema[]>;
28
+ /**
29
+ * The smallest report that proves the whole chain: one metric, one dimension, one row, today.
30
+ *
31
+ * It has to be a real report rather than a metadata call, because the ways GA4 setup fails are
32
+ * only distinguishable when you actually query DATA. The credential can be valid, the Data API
33
+ * can be enabled, and the service account can still be unable to read this property — that last
34
+ * step happens in the Analytics UI, not in Cloud Console, and it is the one people miss. So a
35
+ * permission failure is reported as its own case with the fix in the message, rather than as
36
+ * whatever Google's error string happened to say.
37
+ *
38
+ * On success it reports the property's reporting time zone, because every `date` this connector
39
+ * lands is a day in that zone rather than in UTC, and that is not otherwise discoverable from
40
+ * the synced table.
41
+ */
42
+ TestConnection(companyIntegration: MJCompanyIntegrationEntity, contextUser: UserInfo): Promise<ConnectionTestResult>;
43
+ /**
44
+ * One `runReport` per call — see the 30s note in the class docs.
45
+ *
46
+ * The call resolves its position from the cursor, or opens the run's first window from the
47
+ * watermark. It returns `HasMore: true` with a fresh cursor for as long as there is either more
48
+ * of this window to page or another window to open, and only on the very last page of the very
49
+ * last window does it emit `NewWatermarkValue`.
50
+ *
51
+ * The watermark is the run's PINNED `today`, not the newest date actually seen in the data. Those
52
+ * differ whenever a property has no traffic on its most recent days, and using the data's own
53
+ * maximum would leave the watermark stuck behind a quiet weekend, re-reading the same empty range
54
+ * on every run until traffic resumed.
55
+ */
56
+ FetchChanges(ctx: FetchContext): Promise<FetchBatchResult>;
57
+ /** Resume where the run left off, or open the run's first window from the watermark. */
58
+ private ResolveCursor;
59
+ /** Move to the next date window, or null when the run has reached its pinned `today`. */
60
+ private AdvanceWindow;
61
+ /**
62
+ * Project the response, collecting the conditions worth warning about.
63
+ *
64
+ * These are all things GA4 reports about the DATA rather than about the request, so none of them
65
+ * fails a run — but each one means the landed numbers do not say what they appear to say, and
66
+ * that must not be invisible.
67
+ */
68
+ private ProjectRows;
69
+ /**
70
+ * The service-account key comes from `MJ: Credentials` and nowhere else.
71
+ *
72
+ * Not from `CompanyIntegration.Configuration`, which carries the non-secret half, and not from a
73
+ * process env var — the legacy provider accepted a `GA4_SA_JSON` fallback, which made the
74
+ * effective credential depend on the host's environment rather than on the record, so two
75
+ * companies on one MJAPI could silently read the same property. And explicitly not from
76
+ * `CompanyIntegration.APIKey`: that column is not decrypt-on-read, so an mj-sync-encrypted value
77
+ * comes back as the literal `$ENC$…` string and would be handed to Google verbatim.
78
+ */
79
+ protected LoadServiceAccount(companyIntegration: MJCompanyIntegrationEntity, contextUser: UserInfo, provider?: IMetadataProvider): Promise<GA4ServiceAccount>;
80
+ }
81
+ /** Tree-shaking prevention function — import and call from the module entry point. */
82
+ export declare function LoadGA4Connector(): void;
@@ -0,0 +1,353 @@
1
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
2
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
3
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
4
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
5
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
6
+ };
7
+ /**
8
+ * Google Analytics 4 connector, over the Data API v1beta.
9
+ *
10
+ * This class is ORCHESTRATION ONLY. The report catalog, the config parse, the service-account parse,
11
+ * the window arithmetic and cursor codec, and the row projection all live in pure modules beside it
12
+ * with their own tests. What is left here is the part that needs an engine, a clock and a network.
13
+ *
14
+ * ── WHAT MAKES GA4 UNLIKE THE OTHER CONNECTORS ───────────────────────────────────────────────────
15
+ *
16
+ * There is nothing to list. Every other connector in this repo reads an API that has records —
17
+ * `/projects`, `/users`, a CSV of organizations — and the connector's job is to walk them. GA4 has
18
+ * no records. It has a query engine, and a "report" is whatever you ask it for: a set of dimensions,
19
+ * a set of metrics, and a date range, aggregated on demand. So the objects here are *defined
20
+ * reports*, declared in the catalog, and the identity of a row is the dimension tuple that produced
21
+ * it. That has three consequences that shape everything below.
22
+ *
23
+ * 1. **`date` must be a dimension, on every object.** A report aggregated over a range is one answer
24
+ * whose shape changes whenever the range moves; there is no stable row to upsert. Adding `date`
25
+ * turns it into one row per day, which is the only form with an identity that survives across
26
+ * runs.
27
+ *
28
+ * 2. **A day is not final when it ends.** GA4 keeps reprocessing recent events for up to 48 hours,
29
+ * and user-scoped metrics keep moving for longer. A watermark that advanced strictly forward
30
+ * would land every day exactly once, at its least accurate, and never revisit it — so the window
31
+ * opens `lookbackDays` behind the watermark and re-reads the tail. The engine's content-hash
32
+ * prefetch makes that nearly free: days that did not move cost a read and no write.
33
+ *
34
+ * 3. **The grain is chosen, not given.** Which is why there are three objects rather than one with a
35
+ * rollup — see the long note in `GA4Objects.ts`. Short version: GA4's user metrics are
36
+ * cardinalities, cardinalities are not additive, and a rollup that is right for the counts and
37
+ * quietly wrong for the users is worse than no rollup.
38
+ *
39
+ * ── THE ENGINE CONSTRAINT THIS ANSWERS TO ────────────────────────────────────────────────────────
40
+ *
41
+ * `FetchChangesMs` is 30s, read from a module constant with no per-connector override, and a timeout
42
+ * is classified retryable while the timeout wrapper is a non-cancelling `Promise.race` — so an
43
+ * overrun is not a clean retry, it is the same work running three times concurrently. Every call
44
+ * here therefore issues exactly ONE `runReport` and returns, rather than looping until the batch is
45
+ * full. One request per call is also the honest shape: GA4 pages by offset, and the engine's re-call
46
+ * loop is a perfectly good pump.
47
+ */
48
+ import { RegisterClass } from '@memberjunction/global';
49
+ import { Metadata } from '@memberjunction/core';
50
+ import { BaseIntegrationConnector, } from '@memberjunction/integration-engine';
51
+ import { parseGA4Config, GA4_MAX_LIMIT } from './GA4Config.js';
52
+ import { GA4_OBJECTS, catalogObject, toIntegrationObjectInfo, } from './GA4Objects.js';
53
+ import { parseServiceAccount } from './GA4ServiceAccount.js';
54
+ import { DefaultGA4ReportPort, isPermissionError, isQuotaError, } from './GA4Report.js';
55
+ import { projectRow } from './GA4Rows.js';
56
+ import { formatCursor, initialWindow, nextWindow, parseCursor, toISODate, } from './GA4Window.js';
57
+ let GA4Connector = class GA4Connector extends BaseIntegrationConnector {
58
+ get IntegrationName() {
59
+ return 'Google Analytics 4';
60
+ }
61
+ /** The watermark is a date that only ever advances toward today. */
62
+ get MonotonicWatermark() {
63
+ return true;
64
+ }
65
+ /**
66
+ * GA4 orders a report by its own default and offers no stable seek key. Position is carried by
67
+ * the cursor's offset against a pinned date range instead — which is exact for the duration of a
68
+ * run, where a sort key would only be approximate.
69
+ */
70
+ StableOrderingKey(_objectName) {
71
+ return null;
72
+ }
73
+ /** Overridable so tests drive the clock and the API without a credential or a network. */
74
+ Now() {
75
+ return new Date();
76
+ }
77
+ async Report(companyIntegration, contextUser) {
78
+ const account = await this.LoadServiceAccount(companyIntegration, contextUser);
79
+ return new DefaultGA4ReportPort(account.client_email, account.private_key);
80
+ }
81
+ // ── Discovery ─────────────────────────────────────────────────────────────
82
+ // Answered from the declared catalog rather than from seeded metadata, so the connector is usable
83
+ // BEFORE its metadata has been pushed — which is the state it is in at first setup.
84
+ GetIntegrationObjects() {
85
+ return GA4_OBJECTS.map(toIntegrationObjectInfo);
86
+ }
87
+ async DiscoverObjects(_companyIntegration, _contextUser) {
88
+ return GA4_OBJECTS.map((o) => ({
89
+ Name: o.Name,
90
+ Label: o.DisplayName,
91
+ Description: o.Description,
92
+ SupportsIncrementalSync: o.SupportsIncrementalSync,
93
+ SupportsWrite: false,
94
+ }));
95
+ }
96
+ /**
97
+ * MaxLength/Precision/Scale/IsPrimaryKey are set explicitly, and that is load-bearing. The engine
98
+ * has two bridges from a declared catalog into field schemas and they are not equivalent — one
99
+ * carries these attributes, the other drops them. A `--base` connector that leaves them to be
100
+ * inferred gets unbounded columns and hash-based identity instead of key-based.
101
+ */
102
+ async DiscoverFields(_companyIntegration, objectName, _contextUser) {
103
+ return catalogObject(objectName).Fields.map((f) => ({
104
+ Name: f.Name,
105
+ Label: f.DisplayName,
106
+ Description: f.Description,
107
+ DataType: f.Type,
108
+ IsRequired: f.IsRequired,
109
+ AllowsNull: f.AllowsNull,
110
+ IsPrimaryKey: f.IsPrimaryKey,
111
+ IsUniqueKey: f.IsUniqueKey,
112
+ IsReadOnly: true,
113
+ MaxLength: f.Length ?? null,
114
+ Precision: f.Precision ?? null,
115
+ Scale: f.Scale ?? null,
116
+ }));
117
+ }
118
+ // ── Connection ────────────────────────────────────────────────────────────
119
+ /**
120
+ * The smallest report that proves the whole chain: one metric, one dimension, one row, today.
121
+ *
122
+ * It has to be a real report rather than a metadata call, because the ways GA4 setup fails are
123
+ * only distinguishable when you actually query DATA. The credential can be valid, the Data API
124
+ * can be enabled, and the service account can still be unable to read this property — that last
125
+ * step happens in the Analytics UI, not in Cloud Console, and it is the one people miss. So a
126
+ * permission failure is reported as its own case with the fix in the message, rather than as
127
+ * whatever Google's error string happened to say.
128
+ *
129
+ * On success it reports the property's reporting time zone, because every `date` this connector
130
+ * lands is a day in that zone rather than in UTC, and that is not otherwise discoverable from
131
+ * the synced table.
132
+ */
133
+ async TestConnection(companyIntegration, contextUser) {
134
+ let config;
135
+ try {
136
+ config = parseGA4Config(companyIntegration.Configuration);
137
+ }
138
+ catch (e) {
139
+ return { Success: false, Message: e.message };
140
+ }
141
+ let account;
142
+ try {
143
+ account = await this.LoadServiceAccount(companyIntegration, contextUser);
144
+ }
145
+ catch (e) {
146
+ return { Success: false, Message: e.message };
147
+ }
148
+ const today = toISODate(this.Now());
149
+ try {
150
+ const port = await this.Report(companyIntegration, contextUser);
151
+ const response = await port.RunReport({
152
+ property: `properties/${config.propertyId}`,
153
+ dateRanges: [{ startDate: today, endDate: today }],
154
+ dimensions: [{ name: 'date' }],
155
+ metrics: [{ name: 'sessions' }],
156
+ limit: 1,
157
+ offset: 0,
158
+ returnPropertyQuota: true,
159
+ });
160
+ const zone = response.metadata?.timeZone ?? 'unknown';
161
+ return {
162
+ Success: true,
163
+ Message: `Read property ${config.propertyId} as ${account.client_email}. ` +
164
+ `Reporting time zone: ${zone} — every synced 'date' is a day in that zone, not UTC.`,
165
+ ServerVersion: 'v1beta',
166
+ };
167
+ }
168
+ catch (e) {
169
+ if (isPermissionError(e)) {
170
+ return {
171
+ Success: false,
172
+ Message: `The service account ${account.client_email} authenticated, but is not permitted to read GA4 property ${config.propertyId}. ` +
173
+ 'A Cloud IAM role is not sufficient: add that email as a property user in Google Analytics → Admin → Property Access Management with at least Viewer. ' +
174
+ 'Also confirm the Google Analytics Data API is enabled on the service account\'s project.',
175
+ };
176
+ }
177
+ if (isQuotaError(e)) {
178
+ return {
179
+ Success: false,
180
+ Message: `GA4 quota is exhausted for property ${config.propertyId}; the credential itself looks fine. Analytics token buckets refill hourly and daily — retry later. (${e.message})`,
181
+ };
182
+ }
183
+ return {
184
+ Success: false,
185
+ Message: `GA4 runReport failed for property ${config.propertyId}: ${e.message}`,
186
+ };
187
+ }
188
+ }
189
+ // ── Fetch ─────────────────────────────────────────────────────────────────
190
+ /**
191
+ * One `runReport` per call — see the 30s note in the class docs.
192
+ *
193
+ * The call resolves its position from the cursor, or opens the run's first window from the
194
+ * watermark. It returns `HasMore: true` with a fresh cursor for as long as there is either more
195
+ * of this window to page or another window to open, and only on the very last page of the very
196
+ * last window does it emit `NewWatermarkValue`.
197
+ *
198
+ * The watermark is the run's PINNED `today`, not the newest date actually seen in the data. Those
199
+ * differ whenever a property has no traffic on its most recent days, and using the data's own
200
+ * maximum would leave the watermark stuck behind a quiet weekend, re-reading the same empty range
201
+ * on every run until traffic resumed.
202
+ */
203
+ async FetchChanges(ctx) {
204
+ const obj = catalogObject(ctx.ObjectName);
205
+ const config = parseGA4Config(ctx.CompanyIntegration.Configuration);
206
+ const port = await this.Report(ctx.CompanyIntegration, ctx.ContextUser);
207
+ const cursor = this.ResolveCursor(ctx, config);
208
+ const limit = Math.max(1, Math.min(ctx.BatchSize, GA4_MAX_LIMIT));
209
+ const request = {
210
+ property: `properties/${config.propertyId}`,
211
+ dateRanges: [{ startDate: cursor.From, endDate: cursor.To }],
212
+ dimensions: obj.Dimensions.map((name) => ({ name })),
213
+ metrics: obj.Metrics.map((name) => ({ name })),
214
+ limit,
215
+ offset: cursor.Offset,
216
+ returnPropertyQuota: true,
217
+ };
218
+ let response;
219
+ try {
220
+ response = await port.RunReport(request);
221
+ }
222
+ catch (e) {
223
+ // Rethrown with the window attached. A bare Google error names neither the property nor
224
+ // the date range, and "which request failed" is the first thing anyone needs.
225
+ throw new Error(`GA4 runReport failed for ${ctx.ObjectName} on property ${config.propertyId} over ${cursor.From}..${cursor.To} (offset ${cursor.Offset}): ${e.message}`);
226
+ }
227
+ const { records, warnings } = this.ProjectRows(obj, response, config, cursor);
228
+ // GA4 reports the full match count on every page, so exhaustion of a window is knowable
229
+ // without probing for an empty page. Falling back to the consumed count when `rowCount` is
230
+ // absent makes a short page mean "done" rather than looping forever on a missing field, and
231
+ // the `rowsReturned > 0` guard makes an empty page terminal under any response shape.
232
+ const rowsReturned = response.rows?.length ?? 0;
233
+ const consumed = cursor.Offset + rowsReturned;
234
+ const total = typeof response.rowCount === 'number' ? response.rowCount : consumed;
235
+ const next = consumed < total && rowsReturned > 0
236
+ ? { ...cursor, Offset: consumed }
237
+ : this.AdvanceWindow(cursor, config);
238
+ return {
239
+ Records: records,
240
+ HasMore: next !== null,
241
+ NextCursor: next ? formatCursor(next) : undefined,
242
+ // Only when the whole run is done. Advancing mid-run would let a crash mark days ingested
243
+ // that were never read, and the lookback window is not wide enough to recover an
244
+ // arbitrary gap.
245
+ NewWatermarkValue: next === null ? cursor.Today : undefined,
246
+ Warnings: warnings.length > 0 ? warnings : undefined,
247
+ };
248
+ }
249
+ /** Resume where the run left off, or open the run's first window from the watermark. */
250
+ ResolveCursor(ctx, config) {
251
+ const existing = parseCursor(ctx.CurrentCursor);
252
+ if (existing)
253
+ return existing;
254
+ const today = toISODate(this.Now());
255
+ const window = initialWindow(config, ctx.WatermarkValue, today);
256
+ return { Today: today, From: window.From, To: window.To, Offset: 0 };
257
+ }
258
+ /** Move to the next date window, or null when the run has reached its pinned `today`. */
259
+ AdvanceWindow(cursor, config) {
260
+ const window = nextWindow(cursor, config, cursor.Today);
261
+ return window ? { Today: cursor.Today, From: window.From, To: window.To, Offset: 0 } : null;
262
+ }
263
+ /**
264
+ * Project the response, collecting the conditions worth warning about.
265
+ *
266
+ * These are all things GA4 reports about the DATA rather than about the request, so none of them
267
+ * fails a run — but each one means the landed numbers do not say what they appear to say, and
268
+ * that must not be invisible.
269
+ */
270
+ ProjectRows(obj, response, config, cursor) {
271
+ const records = [];
272
+ const warnings = [];
273
+ let unkeyable = 0;
274
+ let otherRows = 0;
275
+ for (const row of response.rows ?? []) {
276
+ const projected = projectRow(obj, row, config.propertyId);
277
+ if (projected === null) {
278
+ unkeyable++;
279
+ continue;
280
+ }
281
+ if (projected.IsOtherRow)
282
+ otherRows++;
283
+ records.push(projected.Record);
284
+ }
285
+ if (unkeyable > 0) {
286
+ warnings.push({
287
+ Code: 'UNKEYABLE_ROW',
288
+ Message: `${unkeyable} row(s) skipped: GA4 returned a value for the 'date' dimension that is not a date, so the row has no stable identity.`,
289
+ Data: { count: unkeyable, window: `${cursor.From}..${cursor.To}` },
290
+ });
291
+ }
292
+ if (response.metadata?.dataLossFromOtherRow || otherRows > 0) {
293
+ // The rows are still emitted — they are real traffic, and dropping them would silently
294
+ // shrink every total computed from this table. The warning is what stops '(other)' from
295
+ // being read as a campaign name.
296
+ warnings.push({
297
+ Code: 'CARDINALITY_LIMIT',
298
+ Message: `GA4 hit a cardinality limit for ${obj.Name} over ${cursor.From}..${cursor.To} and collapsed some rows into an '(other)' aggregate. ` +
299
+ 'Totals stay complete but the detail behind those rows is gone. Lower Configuration.maxWindowDays so each request covers a narrower range.',
300
+ Data: { object: obj.Name, otherRows, window: `${cursor.From}..${cursor.To}` },
301
+ });
302
+ }
303
+ if (response.metadata?.subjectToThresholding) {
304
+ warnings.push({
305
+ Code: 'DATA_THRESHOLDED',
306
+ Message: `GA4 withheld rows for ${obj.Name} over ${cursor.From}..${cursor.To} because the underlying audience was too small to report without identifying individuals. ` +
307
+ 'Thresholding applies when Google signals are enabled; the landed rows undercount by an amount GA4 does not disclose.',
308
+ Data: { object: obj.Name, window: `${cursor.From}..${cursor.To}` },
309
+ });
310
+ }
311
+ const remainingHourly = response.propertyQuota?.tokensPerHour?.remaining;
312
+ if (typeof remainingHourly === 'number' && remainingHourly <= 0) {
313
+ warnings.push({
314
+ Code: 'QUOTA_EXHAUSTED',
315
+ Message: `GA4 analytics tokens for this hour are exhausted on property ${config.propertyId}. Later requests in this run will fail until the bucket refills.`,
316
+ Data: { propertyId: config.propertyId },
317
+ });
318
+ }
319
+ return { records, warnings };
320
+ }
321
+ // ── Credentials ───────────────────────────────────────────────────────────
322
+ /**
323
+ * The service-account key comes from `MJ: Credentials` and nowhere else.
324
+ *
325
+ * Not from `CompanyIntegration.Configuration`, which carries the non-secret half, and not from a
326
+ * process env var — the legacy provider accepted a `GA4_SA_JSON` fallback, which made the
327
+ * effective credential depend on the host's environment rather than on the record, so two
328
+ * companies on one MJAPI could silently read the same property. And explicitly not from
329
+ * `CompanyIntegration.APIKey`: that column is not decrypt-on-read, so an mj-sync-encrypted value
330
+ * comes back as the literal `$ENC$…` string and would be handed to Google verbatim.
331
+ */
332
+ async LoadServiceAccount(companyIntegration, contextUser, provider) {
333
+ const credentialID = companyIntegration.CredentialID;
334
+ if (!credentialID) {
335
+ throw new Error('GA4: CompanyIntegration.CredentialID is not set. Create an MJ: Credentials record of type "Google Service Account" holding the downloaded key JSON and link it — the key is never read from Configuration.');
336
+ }
337
+ const md = provider ?? new Metadata();
338
+ const credential = await md.GetEntityObject('MJ: Credentials', contextUser);
339
+ const loaded = await credential.Load(credentialID);
340
+ if (!loaded) {
341
+ throw new Error(`GA4: credential ${credentialID} could not be loaded. It may have been deleted, or this user may not have access to it.`);
342
+ }
343
+ return parseServiceAccount(credential.Values);
344
+ }
345
+ };
346
+ GA4Connector = __decorate([
347
+ RegisterClass(BaseIntegrationConnector, 'GA4Connector'),
348
+ RegisterClass(BaseIntegrationConnector, '@memberjunction/connector-ga4')
349
+ ], GA4Connector);
350
+ export { GA4Connector };
351
+ /** Tree-shaking prevention function — import and call from the module entry point. */
352
+ export function LoadGA4Connector() { }
353
+ //# sourceMappingURL=GA4Connector.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"GA4Connector.js","sourceRoot":"","sources":["../src/GA4Connector.ts"],"names":[],"mappings":"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"}