@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.
- package/dist/GA4Config.d.ts +72 -0
- package/dist/GA4Config.js +103 -0
- package/dist/GA4Config.js.map +1 -0
- package/dist/GA4Connector.d.ts +82 -0
- package/dist/GA4Connector.js +353 -0
- package/dist/GA4Connector.js.map +1 -0
- package/dist/GA4Objects.d.ts +100 -0
- package/dist/GA4Objects.js +191 -0
- package/dist/GA4Objects.js.map +1 -0
- package/dist/GA4Report.d.ts +109 -0
- package/dist/GA4Report.js +77 -0
- package/dist/GA4Report.js.map +1 -0
- package/dist/GA4Rows.d.ts +63 -0
- package/dist/GA4Rows.js +117 -0
- package/dist/GA4Rows.js.map +1 -0
- package/dist/GA4ServiceAccount.d.ts +40 -0
- package/dist/GA4ServiceAccount.js +96 -0
- package/dist/GA4ServiceAccount.js.map +1 -0
- package/dist/GA4Window.d.ts +61 -0
- package/dist/GA4Window.js +83 -0
- package/dist/GA4Window.js.map +1 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +5 -0
- package/dist/index.js.map +1 -0
- package/package.json +41 -0
package/dist/GA4Rows.js
ADDED
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Projection of a GA4 report row into an `ExternalRecord`.
|
|
3
|
+
*
|
|
4
|
+
* Pure: row in, record out. This is where the two things GA4 does differently from a normal API get
|
|
5
|
+
* handled — values are POSITIONAL rather than named, and everything, including counts, arrives as a
|
|
6
|
+
* string.
|
|
7
|
+
*/
|
|
8
|
+
import { createHash } from 'node:crypto';
|
|
9
|
+
/**
|
|
10
|
+
* Upper bound for `ExternalID`.
|
|
11
|
+
*
|
|
12
|
+
* `CompanyIntegrationRecordMap.ExternalSystemRecordID` is `nvarchar(750)`. A key that overflows it
|
|
13
|
+
* does not corrupt anything — the insert fails and the record is dead-lettered — but the row never
|
|
14
|
+
* lands, silently, for as long as the offending value keeps appearing. Below this length the key is
|
|
15
|
+
* the readable join; above it, {@link buildExternalID} substitutes a digest. The margin under 750 is
|
|
16
|
+
* for the fallback prefix.
|
|
17
|
+
*/
|
|
18
|
+
export const MAX_EXTERNAL_ID_LENGTH = 700;
|
|
19
|
+
/** GA4's sentinel for rows collapsed together once a dimension exceeds its cardinality limit. */
|
|
20
|
+
export const OTHER_ROW_SENTINEL = '(other)';
|
|
21
|
+
/**
|
|
22
|
+
* Build the `ExternalID` from the declared key values.
|
|
23
|
+
*
|
|
24
|
+
* It must equal the key fields joined on '|' — that is what the engine's REST base class produces
|
|
25
|
+
* and what a `--base` connector therefore has to reproduce by hand. A mismatch does not throw:
|
|
26
|
+
* identity silently falls back to a content hash, and every run re-inserts every row.
|
|
27
|
+
*
|
|
28
|
+
* The digest fallback exists because two key components here are free text set by whoever built a
|
|
29
|
+
* marketing link. `utm_campaign` and `utm_content` have no length limit, and a tracking template
|
|
30
|
+
* that stuffs a few hundred characters into one is not exotic. Truncating would be the worse answer:
|
|
31
|
+
* two distinct campaigns sharing a prefix would collapse into one row and silently merge their
|
|
32
|
+
* numbers, which is a wrong answer rather than a missing one. The digest is deterministic, so the
|
|
33
|
+
* same row keeps the same identity across runs, and the `ga4:` prefix makes an audit of the record
|
|
34
|
+
* map able to tell the two forms apart.
|
|
35
|
+
*/
|
|
36
|
+
export function buildExternalID(keyValues) {
|
|
37
|
+
const joined = keyValues.join('|');
|
|
38
|
+
if (joined.length <= MAX_EXTERNAL_ID_LENGTH)
|
|
39
|
+
return joined;
|
|
40
|
+
return `ga4:${createHash('sha256').update(joined, 'utf8').digest('hex')}`;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* GA4's `date` dimension is `YYYYMMDD` with no separators. Returns null for anything else — including
|
|
44
|
+
* `(other)`, which GA4 can in principle substitute for any dimension.
|
|
45
|
+
*/
|
|
46
|
+
export function parseGA4Date(value) {
|
|
47
|
+
if (!/^\d{8}$/.test(value))
|
|
48
|
+
return null;
|
|
49
|
+
const iso = `${value.slice(0, 4)}-${value.slice(4, 6)}-${value.slice(6, 8)}`;
|
|
50
|
+
// Rejects 2026-02-31 and friends: Date.parse accepts them and rolls over, which would land a row
|
|
51
|
+
// under a date GA4 never reported.
|
|
52
|
+
const parsed = new Date(`${iso}T00:00:00Z`);
|
|
53
|
+
return Number.isNaN(parsed.getTime()) || parsed.toISOString().slice(0, 10) !== iso ? null : iso;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Parse a GA4 metric value.
|
|
57
|
+
*
|
|
58
|
+
* Every metric arrives as a string, including integers, and a metric that is genuinely absent for a
|
|
59
|
+
* row arrives as `''` rather than being omitted. Absent stays null (the column is nullable and "no
|
|
60
|
+
* data" is not the same claim as "zero"); anything present but unparseable becomes null too, rather
|
|
61
|
+
* than a 0 that would read as a measured value.
|
|
62
|
+
*/
|
|
63
|
+
export function parseMetric(value) {
|
|
64
|
+
if (value === null || value === undefined || value === '')
|
|
65
|
+
return null;
|
|
66
|
+
const n = Number(value);
|
|
67
|
+
return Number.isFinite(n) ? n : null;
|
|
68
|
+
}
|
|
69
|
+
/** Positional read of a nullable-everything array, flattened to a plain string. */
|
|
70
|
+
function valueAt(list, i) {
|
|
71
|
+
return list?.[i]?.value ?? '';
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Project one report row.
|
|
75
|
+
*
|
|
76
|
+
* Returns null when the row cannot be keyed — in practice only when `date` is not a real date, which
|
|
77
|
+
* means GA4 substituted a sentinel for it. A row with no usable key cannot be upserted, and emitting
|
|
78
|
+
* it with a made-up key would create a row that is re-inserted on every run forever.
|
|
79
|
+
*
|
|
80
|
+
* `dimensionValues` and `metricValues` are aligned by POSITION to the request's `dimensions` and
|
|
81
|
+
* `metrics`, not by name — GA4 sends the names once in the headers and never again. The catalog's
|
|
82
|
+
* `Dimensions`/`Metrics` arrays are the request order, so they are also the read order, and that
|
|
83
|
+
* single fact is why those arrays live next to the field list rather than being derived from it.
|
|
84
|
+
*/
|
|
85
|
+
export function projectRow(obj, row, propertyId) {
|
|
86
|
+
const fields = {};
|
|
87
|
+
let isOtherRow = false;
|
|
88
|
+
for (let i = 0; i < obj.Dimensions.length; i++) {
|
|
89
|
+
const name = obj.Dimensions[i];
|
|
90
|
+
const raw = valueAt(row.dimensionValues, i);
|
|
91
|
+
if (raw === OTHER_ROW_SENTINEL)
|
|
92
|
+
isOtherRow = true;
|
|
93
|
+
if (name === 'date') {
|
|
94
|
+
const iso = parseGA4Date(raw);
|
|
95
|
+
if (iso === null)
|
|
96
|
+
return null;
|
|
97
|
+
fields.date = iso;
|
|
98
|
+
}
|
|
99
|
+
else {
|
|
100
|
+
fields[name] = raw;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
for (let i = 0; i < obj.Metrics.length; i++) {
|
|
104
|
+
fields[obj.Metrics[i]] = parseMetric(valueAt(row.metricValues, i));
|
|
105
|
+
}
|
|
106
|
+
fields.propertyId = propertyId;
|
|
107
|
+
const keyValues = obj.Fields.filter((f) => f.IsPrimaryKey).map((f) => String(fields[f.Name] ?? ''));
|
|
108
|
+
return {
|
|
109
|
+
Record: {
|
|
110
|
+
ExternalID: buildExternalID(keyValues),
|
|
111
|
+
ObjectType: obj.Name,
|
|
112
|
+
Fields: fields,
|
|
113
|
+
},
|
|
114
|
+
IsOtherRow: isOtherRow,
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
//# sourceMappingURL=GA4Rows.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"GA4Rows.js","sourceRoot":"","sources":["../src/GA4Rows.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Parsing and validation of the Google service-account key.
|
|
3
|
+
*
|
|
4
|
+
* Pure and dependency-free, because every interesting failure here happens at setup time on someone
|
|
5
|
+
* else's machine and the only way to make those failures cheap is to name them precisely. A service
|
|
6
|
+
* account that authenticates but cannot read the property, or a key whose newlines were eaten in
|
|
7
|
+
* transit, both surface as an opaque Google error several layers down; caught here they are one
|
|
8
|
+
* sentence each.
|
|
9
|
+
*/
|
|
10
|
+
/** The fields of a Google service-account JSON this connector actually uses. */
|
|
11
|
+
export interface GA4ServiceAccount {
|
|
12
|
+
client_email: string;
|
|
13
|
+
private_key: string;
|
|
14
|
+
project_id?: string;
|
|
15
|
+
private_key_id?: string;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* The shapes a credential can legitimately arrive in.
|
|
19
|
+
*
|
|
20
|
+
* `MJ: Credentials.Values` is a JSON document, and how an operator puts a service-account key into
|
|
21
|
+
* it varies: pasted as a nested object, pasted as an escaped string, or — because the downloaded
|
|
22
|
+
* file IS the whole credential — pasted as the service-account JSON itself with no wrapper. All
|
|
23
|
+
* three are accepted. Rejecting two of them would be pure ceremony: they are unambiguous, and the
|
|
24
|
+
* alternative is a support conversation about JSON nesting.
|
|
25
|
+
*/
|
|
26
|
+
export declare function parseServiceAccount(values: string | null | undefined): GA4ServiceAccount;
|
|
27
|
+
/**
|
|
28
|
+
* Restore real newlines in a PEM key.
|
|
29
|
+
*
|
|
30
|
+
* A service-account key is a multi-line PEM stored inside a JSON string, so it legitimately contains
|
|
31
|
+
* `\n` escapes. Whether those survive as escapes or arrive already-unescaped depends on how many
|
|
32
|
+
* times the value has been through a JSON round-trip on its way here — via an env var, a shell
|
|
33
|
+
* variable, a form field, a copy-paste. Both forms are common and only one of them is a valid key,
|
|
34
|
+
* so the escaped form is converted rather than rejected. This is the single most common GA4
|
|
35
|
+
* credential failure and it otherwise surfaces as "error:1E08010C:DECODER routines::unsupported".
|
|
36
|
+
*
|
|
37
|
+
* The order matters: `\\n` (an escaped backslash followed by n, i.e. a literal backslash in the key)
|
|
38
|
+
* is left alone, and only a lone `\n` escape becomes a newline.
|
|
39
|
+
*/
|
|
40
|
+
export declare function normalizePrivateKey(key: string): string;
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Parsing and validation of the Google service-account key.
|
|
3
|
+
*
|
|
4
|
+
* Pure and dependency-free, because every interesting failure here happens at setup time on someone
|
|
5
|
+
* else's machine and the only way to make those failures cheap is to name them precisely. A service
|
|
6
|
+
* account that authenticates but cannot read the property, or a key whose newlines were eaten in
|
|
7
|
+
* transit, both surface as an opaque Google error several layers down; caught here they are one
|
|
8
|
+
* sentence each.
|
|
9
|
+
*/
|
|
10
|
+
/**
|
|
11
|
+
* The shapes a credential can legitimately arrive in.
|
|
12
|
+
*
|
|
13
|
+
* `MJ: Credentials.Values` is a JSON document, and how an operator puts a service-account key into
|
|
14
|
+
* it varies: pasted as a nested object, pasted as an escaped string, or — because the downloaded
|
|
15
|
+
* file IS the whole credential — pasted as the service-account JSON itself with no wrapper. All
|
|
16
|
+
* three are accepted. Rejecting two of them would be pure ceremony: they are unambiguous, and the
|
|
17
|
+
* alternative is a support conversation about JSON nesting.
|
|
18
|
+
*/
|
|
19
|
+
export function parseServiceAccount(values) {
|
|
20
|
+
if (!values || !values.trim()) {
|
|
21
|
+
throw new Error('GA4: no credential is linked. Set CompanyIntegration.CredentialID to an MJ: Credentials record whose Values hold the Google service-account JSON, e.g. {"serviceAccountJSON": { ... }}.');
|
|
22
|
+
}
|
|
23
|
+
const root = parseJSON(values, 'the credential Values');
|
|
24
|
+
// Wrapped under a key, or the service-account JSON at the top level.
|
|
25
|
+
const candidate = pickObject(root, 'serviceAccountJSON') ??
|
|
26
|
+
pickObject(root, 'ServiceAccountJSON') ??
|
|
27
|
+
pickObject(root, 'serviceAccountKey') ??
|
|
28
|
+
root;
|
|
29
|
+
const clientEmail = typeof candidate.client_email === 'string' ? candidate.client_email.trim() : '';
|
|
30
|
+
const privateKeyRaw = typeof candidate.private_key === 'string' ? candidate.private_key : '';
|
|
31
|
+
if (!clientEmail || !privateKeyRaw) {
|
|
32
|
+
throw new Error('GA4: the linked credential does not contain a Google service-account key. Expected client_email and private_key — either at the top level of Values, or under a "serviceAccountJSON" key. ' +
|
|
33
|
+
'Use the JSON file downloaded from Google Cloud Console → IAM & Admin → Service Accounts → Keys, verbatim.');
|
|
34
|
+
}
|
|
35
|
+
const privateKey = normalizePrivateKey(privateKeyRaw);
|
|
36
|
+
if (!privateKey.includes('BEGIN PRIVATE KEY')) {
|
|
37
|
+
throw new Error('GA4: the credential\'s private_key does not look like a PEM key (no "BEGIN PRIVATE KEY" header). It is likely truncated, or the private_key_id was pasted in its place.');
|
|
38
|
+
}
|
|
39
|
+
return {
|
|
40
|
+
client_email: clientEmail,
|
|
41
|
+
private_key: privateKey,
|
|
42
|
+
project_id: typeof candidate.project_id === 'string' ? candidate.project_id : undefined,
|
|
43
|
+
private_key_id: typeof candidate.private_key_id === 'string' ? candidate.private_key_id : undefined,
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Restore real newlines in a PEM key.
|
|
48
|
+
*
|
|
49
|
+
* A service-account key is a multi-line PEM stored inside a JSON string, so it legitimately contains
|
|
50
|
+
* `\n` escapes. Whether those survive as escapes or arrive already-unescaped depends on how many
|
|
51
|
+
* times the value has been through a JSON round-trip on its way here — via an env var, a shell
|
|
52
|
+
* variable, a form field, a copy-paste. Both forms are common and only one of them is a valid key,
|
|
53
|
+
* so the escaped form is converted rather than rejected. This is the single most common GA4
|
|
54
|
+
* credential failure and it otherwise surfaces as "error:1E08010C:DECODER routines::unsupported".
|
|
55
|
+
*
|
|
56
|
+
* The order matters: `\\n` (an escaped backslash followed by n, i.e. a literal backslash in the key)
|
|
57
|
+
* is left alone, and only a lone `\n` escape becomes a newline.
|
|
58
|
+
*/
|
|
59
|
+
export function normalizePrivateKey(key) {
|
|
60
|
+
const unescaped = key.includes('\n') ? key : key.replace(/\\n/g, '\n');
|
|
61
|
+
// Windows line endings inside a PEM are tolerated by OpenSSL but not by every parser in the
|
|
62
|
+
// chain; normalizing costs nothing and removes a whole class of "works on my machine".
|
|
63
|
+
return unescaped.replace(/\r\n/g, '\n').trim();
|
|
64
|
+
}
|
|
65
|
+
function parseJSON(text, what) {
|
|
66
|
+
let parsed;
|
|
67
|
+
try {
|
|
68
|
+
parsed = JSON.parse(text);
|
|
69
|
+
}
|
|
70
|
+
catch (e) {
|
|
71
|
+
throw new Error(`GA4: could not parse ${what} as JSON: ${e.message}`);
|
|
72
|
+
}
|
|
73
|
+
if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
74
|
+
throw new Error(`GA4: ${what} must be a JSON object.`);
|
|
75
|
+
}
|
|
76
|
+
return parsed;
|
|
77
|
+
}
|
|
78
|
+
function pickObject(root, key) {
|
|
79
|
+
const v = root[key];
|
|
80
|
+
if (v !== null && typeof v === 'object' && !Array.isArray(v))
|
|
81
|
+
return v;
|
|
82
|
+
// Also accept the nested key stored as an escaped JSON string.
|
|
83
|
+
if (typeof v === 'string' && v.trim().startsWith('{')) {
|
|
84
|
+
try {
|
|
85
|
+
const inner = JSON.parse(v);
|
|
86
|
+
if (inner !== null && typeof inner === 'object' && !Array.isArray(inner)) {
|
|
87
|
+
return inner;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
catch {
|
|
91
|
+
return null;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
return null;
|
|
95
|
+
}
|
|
96
|
+
//# sourceMappingURL=GA4ServiceAccount.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"GA4ServiceAccount.js","sourceRoot":"","sources":["../src/GA4ServiceAccount.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Date-window arithmetic and the pagination cursor.
|
|
3
|
+
*
|
|
4
|
+
* A GA4 report is addressed by a date range plus an offset into the result, so those three values
|
|
5
|
+
* — `from`, `to`, `offset` — are the whole of this connector's position in its source. They travel
|
|
6
|
+
* in the cursor rather than being recomputed per call, and that is load-bearing: the range is derived
|
|
7
|
+
* from the watermark and the clock, the engine does not advance the watermark until a run ends, and
|
|
8
|
+
* so a run that happens to cross midnight would otherwise recompute a DIFFERENT range on its second
|
|
9
|
+
* page and resume an offset into it. Offsets are only meaningful against the range that produced
|
|
10
|
+
* them.
|
|
11
|
+
*
|
|
12
|
+
* The cursor therefore also carries the run's own `today`, pinning the clock for the whole run.
|
|
13
|
+
*
|
|
14
|
+
* Format: `<today>|<from>|<to>|<offset>`, all dates `YYYY-MM-DD`.
|
|
15
|
+
*/
|
|
16
|
+
import type { GA4Config } from './GA4Config.js';
|
|
17
|
+
/** An inclusive date range, GA4's `dateRanges` semantics. */
|
|
18
|
+
export interface GA4Window {
|
|
19
|
+
From: string;
|
|
20
|
+
To: string;
|
|
21
|
+
}
|
|
22
|
+
export interface GA4Cursor extends GA4Window {
|
|
23
|
+
/** The run's pinned clock. Every window in the run is derived from this, not from `new Date()`. */
|
|
24
|
+
Today: string;
|
|
25
|
+
/** Rows already consumed within `[From, To]`. */
|
|
26
|
+
Offset: number;
|
|
27
|
+
}
|
|
28
|
+
/** The UTC date of an instant, `YYYY-MM-DD`. */
|
|
29
|
+
export declare function toISODate(d: Date): string;
|
|
30
|
+
/** Shift an ISO date by whole days. Returns the input unchanged if it is not a valid ISO date. */
|
|
31
|
+
export declare function addDays(iso: string, days: number): string;
|
|
32
|
+
/**
|
|
33
|
+
* The first window of a run.
|
|
34
|
+
*
|
|
35
|
+
* With a watermark, the window opens `lookbackDays` BEFORE it — re-reading days that are already
|
|
36
|
+
* landed, because GA4 keeps revising recent days for up to 48 hours and a strictly-forward watermark
|
|
37
|
+
* would freeze each day at its least accurate value forever.
|
|
38
|
+
*
|
|
39
|
+
* Without one, the window opens at the configured `startDate`, or {@link DEFAULT_COLD_START_DAYS}
|
|
40
|
+
* back when there is none.
|
|
41
|
+
*
|
|
42
|
+
* The `startDate` floor is applied in BOTH branches on purpose: it is a statement about what this
|
|
43
|
+
* property is allowed to be read for, and the lookback must not be able to reach behind it.
|
|
44
|
+
*/
|
|
45
|
+
export declare function initialWindow(config: GA4Config, watermark: string | null, today: string): GA4Window;
|
|
46
|
+
/**
|
|
47
|
+
* The window after this one, or null when the run has reached `today`.
|
|
48
|
+
*
|
|
49
|
+
* Advancing here rather than ending the run is what lets a cold backfill of a year finish in a single
|
|
50
|
+
* run: `maxWindowDays` bounds each REQUEST, not the run.
|
|
51
|
+
*/
|
|
52
|
+
export declare function nextWindow(current: GA4Window, config: GA4Config, today: string): GA4Window | null;
|
|
53
|
+
export declare function formatCursor(c: GA4Cursor): string;
|
|
54
|
+
/**
|
|
55
|
+
* Parse a cursor, or null when it is absent or malformed.
|
|
56
|
+
*
|
|
57
|
+
* Null means "start this object over from the watermark", which is always safe: identity is the
|
|
58
|
+
* declared key, so re-reading rows upserts them. Throwing instead would strand the object on a bad
|
|
59
|
+
* cursor with no way forward short of clearing it by hand.
|
|
60
|
+
*/
|
|
61
|
+
export declare function parseCursor(raw: string | null | undefined): GA4Cursor | null;
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { DEFAULT_COLD_START_DAYS } from './GA4Config.js';
|
|
2
|
+
const SEP = '|';
|
|
3
|
+
const ISO_DATE = /^\d{4}-\d{2}-\d{2}$/;
|
|
4
|
+
const MS_PER_DAY = 86_400_000;
|
|
5
|
+
/** The UTC date of an instant, `YYYY-MM-DD`. */
|
|
6
|
+
export function toISODate(d) {
|
|
7
|
+
return d.toISOString().slice(0, 10);
|
|
8
|
+
}
|
|
9
|
+
/** Shift an ISO date by whole days. Returns the input unchanged if it is not a valid ISO date. */
|
|
10
|
+
export function addDays(iso, days) {
|
|
11
|
+
const base = Date.parse(`${iso}T00:00:00Z`);
|
|
12
|
+
if (Number.isNaN(base))
|
|
13
|
+
return iso;
|
|
14
|
+
return new Date(base + days * MS_PER_DAY).toISOString().slice(0, 10);
|
|
15
|
+
}
|
|
16
|
+
/** Lexicographic comparison is date comparison for `YYYY-MM-DD`, which is why this format is used throughout. */
|
|
17
|
+
const minDate = (a, b) => (a < b ? a : b);
|
|
18
|
+
const maxDate = (a, b) => (a > b ? a : b);
|
|
19
|
+
/**
|
|
20
|
+
* The first window of a run.
|
|
21
|
+
*
|
|
22
|
+
* With a watermark, the window opens `lookbackDays` BEFORE it — re-reading days that are already
|
|
23
|
+
* landed, because GA4 keeps revising recent days for up to 48 hours and a strictly-forward watermark
|
|
24
|
+
* would freeze each day at its least accurate value forever.
|
|
25
|
+
*
|
|
26
|
+
* Without one, the window opens at the configured `startDate`, or {@link DEFAULT_COLD_START_DAYS}
|
|
27
|
+
* back when there is none.
|
|
28
|
+
*
|
|
29
|
+
* The `startDate` floor is applied in BOTH branches on purpose: it is a statement about what this
|
|
30
|
+
* property is allowed to be read for, and the lookback must not be able to reach behind it.
|
|
31
|
+
*/
|
|
32
|
+
export function initialWindow(config, watermark, today) {
|
|
33
|
+
const coldStart = config.startDate ?? addDays(today, -DEFAULT_COLD_START_DAYS);
|
|
34
|
+
let from = watermark && ISO_DATE.test(watermark.slice(0, 10))
|
|
35
|
+
? addDays(watermark.slice(0, 10), -config.lookbackDays)
|
|
36
|
+
: coldStart;
|
|
37
|
+
if (config.startDate)
|
|
38
|
+
from = maxDate(from, config.startDate);
|
|
39
|
+
from = minDate(from, today);
|
|
40
|
+
return { From: from, To: windowEnd(from, config, today) };
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* The window after this one, or null when the run has reached `today`.
|
|
44
|
+
*
|
|
45
|
+
* Advancing here rather than ending the run is what lets a cold backfill of a year finish in a single
|
|
46
|
+
* run: `maxWindowDays` bounds each REQUEST, not the run.
|
|
47
|
+
*/
|
|
48
|
+
export function nextWindow(current, config, today) {
|
|
49
|
+
if (current.To >= today)
|
|
50
|
+
return null;
|
|
51
|
+
const from = addDays(current.To, 1);
|
|
52
|
+
return { From: from, To: windowEnd(from, config, today) };
|
|
53
|
+
}
|
|
54
|
+
function windowEnd(from, config, today) {
|
|
55
|
+
return minDate(addDays(from, config.maxWindowDays - 1), today);
|
|
56
|
+
}
|
|
57
|
+
export function formatCursor(c) {
|
|
58
|
+
return [c.Today, c.From, c.To, c.Offset].join(SEP);
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Parse a cursor, or null when it is absent or malformed.
|
|
62
|
+
*
|
|
63
|
+
* Null means "start this object over from the watermark", which is always safe: identity is the
|
|
64
|
+
* declared key, so re-reading rows upserts them. Throwing instead would strand the object on a bad
|
|
65
|
+
* cursor with no way forward short of clearing it by hand.
|
|
66
|
+
*/
|
|
67
|
+
export function parseCursor(raw) {
|
|
68
|
+
if (!raw)
|
|
69
|
+
return null;
|
|
70
|
+
const parts = raw.split(SEP);
|
|
71
|
+
if (parts.length !== 4)
|
|
72
|
+
return null;
|
|
73
|
+
const [today, from, to, offsetRaw] = parts;
|
|
74
|
+
if (!ISO_DATE.test(today) || !ISO_DATE.test(from) || !ISO_DATE.test(to))
|
|
75
|
+
return null;
|
|
76
|
+
if (from > to)
|
|
77
|
+
return null;
|
|
78
|
+
const offset = Number(offsetRaw);
|
|
79
|
+
if (!Number.isInteger(offset) || offset < 0)
|
|
80
|
+
return null;
|
|
81
|
+
return { Today: today, From: from, To: to, Offset: offset };
|
|
82
|
+
}
|
|
83
|
+
//# sourceMappingURL=GA4Window.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"GA4Window.js","sourceRoot":"","sources":["../src/GA4Window.ts"],"names":[],"mappings":"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"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export * from './GA4Connector.js';
|
|
2
|
+
/** Open App bootstrap entry: importing this module ran the connector's @RegisterClass decorator;
|
|
3
|
+
* this no-op satisfies the loader's required startupExport and forces the import at MJAPI boot. */
|
|
4
|
+
export declare function registerConnector(): void;
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export * from './GA4Connector.js';
|
|
2
|
+
/** Open App bootstrap entry: importing this module ran the connector's @RegisterClass decorator;
|
|
3
|
+
* this no-op satisfies the loader's required startupExport and forces the import at MJAPI boot. */
|
|
4
|
+
export function registerConnector() { }
|
|
5
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,mBAAmB,CAAC;AAElC;oGACoG;AACpG,MAAM,UAAU,iBAAiB,KAAiD,CAAC"}
|
package/package.json
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@memberjunction/connector-ga4",
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "MemberJunction Google Analytics 4 connector (Data API v1beta).",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "dist/index.js",
|
|
7
|
+
"types": "dist/index.d.ts",
|
|
8
|
+
"files": [
|
|
9
|
+
"/dist"
|
|
10
|
+
],
|
|
11
|
+
"scripts": {
|
|
12
|
+
"build": "tsc && tsc-alias -f",
|
|
13
|
+
"test": "vitest run"
|
|
14
|
+
},
|
|
15
|
+
"author": "MemberJunction.com",
|
|
16
|
+
"license": "ISC",
|
|
17
|
+
"peerDependencies": {
|
|
18
|
+
"@memberjunction/core": ">=5.43.0 <6.0.0",
|
|
19
|
+
"@memberjunction/core-entities": ">=5.43.0 <6.0.0",
|
|
20
|
+
"@memberjunction/global": ">=5.43.0 <6.0.0",
|
|
21
|
+
"@memberjunction/integration-engine": ">=5.43.0 <6.0.0"
|
|
22
|
+
},
|
|
23
|
+
"dependencies": {
|
|
24
|
+
"@google-analytics/data": "^7.0.0",
|
|
25
|
+
"zod": "~3.24.4"
|
|
26
|
+
},
|
|
27
|
+
"devDependencies": {
|
|
28
|
+
"@memberjunction/core": "^5.43.0",
|
|
29
|
+
"@memberjunction/core-entities": "^5.43.0",
|
|
30
|
+
"@memberjunction/global": "^5.43.0",
|
|
31
|
+
"@memberjunction/integration-engine": "^5.43.0",
|
|
32
|
+
"@types/node": "24.10.11",
|
|
33
|
+
"tsc-alias": "^1.8.16",
|
|
34
|
+
"typescript": "^5.9.3",
|
|
35
|
+
"vitest": "^4.0.18"
|
|
36
|
+
},
|
|
37
|
+
"repository": {
|
|
38
|
+
"type": "git",
|
|
39
|
+
"url": "https://github.com/MemberJunction/Integrations"
|
|
40
|
+
}
|
|
41
|
+
}
|