mcp-scraper 0.72.7 → 0.73.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/CHANGELOG.md +26 -1
- package/README.md +16 -2
- package/dist/{analytics-repository-6YLRSWYO.js → analytics-repository-MSLJBHLU.js} +15 -1
- package/dist/bin/api-server.js +3 -3
- package/dist/bin/mcp-scraper-cli.js +1 -1
- package/dist/bin/mcp-scraper-install.js +2 -2
- package/dist/bin/mcp-stdio-server.js +4 -4
- package/dist/{chunk-SR7GSLEA.js → chunk-5EWXRBEH.js} +816 -113
- package/dist/{chunk-FVL4GUTP.js → chunk-EQJRPDUS.js} +2 -2
- package/dist/{chunk-RRZAYUEX.js → chunk-JIWGG7Z4.js} +1 -1
- package/dist/{chunk-ZSVXNLLM.js → chunk-KRIIVDV2.js} +188 -14
- package/dist/{chunk-RAFQEPJ4.js → chunk-Q2J23DHY.js} +1 -1
- package/dist/{chunk-I45JV4EU.js → chunk-RV2D27R4.js} +1 -1
- package/dist/{chunk-K2SXQGKM.js → chunk-S45DGELW.js} +1 -1
- package/dist/{extract-bundle-337ZDIIQ.js → extract-bundle-2V22S7EU.js} +2 -2
- package/dist/{gmail-service-QXDGNWUJ.js → gmail-service-JHL4PQFN.js} +2 -2
- package/dist/{server-M5FLUWYD.js → server-N6MCRSQW.js} +14433 -12846
- package/dist/{site-extract-repository-ACNAVNDN.js → site-extract-repository-DUAEAU6A.js} +2 -2
- package/dist/{worker-JQORPCFQ.js → worker-CBK5DWEE.js} +1 -1
- package/package.json +16 -4
- package/dist/bin/api-server.cjs +0 -108656
- package/dist/bin/api-server.d.cts +0 -1
- package/dist/bin/api-server.d.ts +0 -1
- package/dist/bin/mcp-scraper-cli.cjs +0 -3219
- package/dist/bin/mcp-scraper-cli.d.cts +0 -1
- package/dist/bin/mcp-scraper-cli.d.ts +0 -1
- package/dist/bin/mcp-scraper-install.cjs +0 -134
- package/dist/bin/mcp-scraper-install.d.cts +0 -1
- package/dist/bin/mcp-scraper-install.d.ts +0 -1
- package/dist/bin/mcp-stdio-server.cjs +0 -22333
- package/dist/bin/mcp-stdio-server.d.cts +0 -1
- package/dist/bin/mcp-stdio-server.d.ts +0 -1
- package/dist/bin/paa-harvest.cjs +0 -4745
- package/dist/bin/paa-harvest.d.cts +0 -1
- package/dist/bin/paa-harvest.d.ts +0 -1
|
@@ -4,7 +4,7 @@ import {
|
|
|
4
4
|
} from "./chunk-PJEEKOUM.js";
|
|
5
5
|
|
|
6
6
|
// src/api/analytics-repository.ts
|
|
7
|
-
import { createHash, createHmac, randomBytes, randomUUID } from "crypto";
|
|
7
|
+
import { createHash as createHash2, createHmac as createHmac2, randomBytes as randomBytes2, randomUUID as randomUUID2 } from "crypto";
|
|
8
8
|
import { Pool } from "pg";
|
|
9
9
|
|
|
10
10
|
// src/api/analytics-attribution.ts
|
|
@@ -82,6 +82,261 @@ function resolveAnalyticsAttribution(input) {
|
|
|
82
82
|
};
|
|
83
83
|
}
|
|
84
84
|
|
|
85
|
+
// src/api/analytics-identity-profile.ts
|
|
86
|
+
import { createHash, randomUUID } from "crypto";
|
|
87
|
+
|
|
88
|
+
// src/api/analytics-crypto.ts
|
|
89
|
+
import {
|
|
90
|
+
createCipheriv,
|
|
91
|
+
createDecipheriv,
|
|
92
|
+
createHmac,
|
|
93
|
+
randomBytes,
|
|
94
|
+
scryptSync,
|
|
95
|
+
timingSafeEqual
|
|
96
|
+
} from "crypto";
|
|
97
|
+
var VERSION = "xray1";
|
|
98
|
+
function rootSecret() {
|
|
99
|
+
return process.env.ANALYTICS_RESTRICTED_DATA_SECRET?.trim() || getSessionSecret();
|
|
100
|
+
}
|
|
101
|
+
function keyFor(purpose) {
|
|
102
|
+
return scryptSync(rootSecret(), `xray-restricted:${purpose}:v1`, 32);
|
|
103
|
+
}
|
|
104
|
+
function encryptAnalyticsRestricted(plaintext, purpose) {
|
|
105
|
+
const normalizedPurpose = purpose.trim().slice(0, 80);
|
|
106
|
+
if (!normalizedPurpose) throw new Error("analytics encryption purpose required");
|
|
107
|
+
const iv = randomBytes(12);
|
|
108
|
+
const cipher = createCipheriv("aes-256-gcm", keyFor(normalizedPurpose), iv);
|
|
109
|
+
cipher.setAAD(Buffer.from(`${VERSION}:${normalizedPurpose}`, "utf8"));
|
|
110
|
+
const ciphertext = Buffer.concat([
|
|
111
|
+
cipher.update(plaintext, "utf8"),
|
|
112
|
+
cipher.final()
|
|
113
|
+
]);
|
|
114
|
+
const tag = cipher.getAuthTag();
|
|
115
|
+
return [
|
|
116
|
+
VERSION,
|
|
117
|
+
Buffer.from(normalizedPurpose, "utf8").toString("base64url"),
|
|
118
|
+
iv.toString("base64url"),
|
|
119
|
+
tag.toString("base64url"),
|
|
120
|
+
ciphertext.toString("base64url")
|
|
121
|
+
].join(".");
|
|
122
|
+
}
|
|
123
|
+
function decryptAnalyticsRestricted(stored, expectedPurpose) {
|
|
124
|
+
try {
|
|
125
|
+
const [version, encodedPurpose, iv, tag, ciphertext] = stored.split(".");
|
|
126
|
+
if (version !== VERSION || !encodedPurpose || !iv || !tag || !ciphertext)
|
|
127
|
+
return null;
|
|
128
|
+
const purpose = Buffer.from(encodedPurpose, "base64url").toString("utf8");
|
|
129
|
+
if (purpose !== expectedPurpose) return null;
|
|
130
|
+
const decipher = createDecipheriv(
|
|
131
|
+
"aes-256-gcm",
|
|
132
|
+
keyFor(purpose),
|
|
133
|
+
Buffer.from(iv, "base64url")
|
|
134
|
+
);
|
|
135
|
+
decipher.setAAD(Buffer.from(`${VERSION}:${purpose}`, "utf8"));
|
|
136
|
+
decipher.setAuthTag(Buffer.from(tag, "base64url"));
|
|
137
|
+
return Buffer.concat([
|
|
138
|
+
decipher.update(Buffer.from(ciphertext, "base64url")),
|
|
139
|
+
decipher.final()
|
|
140
|
+
]).toString("utf8");
|
|
141
|
+
} catch {
|
|
142
|
+
return null;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
function analyticsIdentityHmac(kind, value) {
|
|
146
|
+
return createHmac("sha256", keyFor(`identity:${kind}`)).update(value.trim().toLowerCase()).digest("hex");
|
|
147
|
+
}
|
|
148
|
+
function signAnalyticsWebhook(provider, timestamp, rawBody, secret) {
|
|
149
|
+
return createHmac("sha256", secret).update(`${provider}.${timestamp}.${rawBody}`).digest("hex");
|
|
150
|
+
}
|
|
151
|
+
function verifyAnalyticsWebhookSignature(actual, expected) {
|
|
152
|
+
if (!actual || actual.length !== expected.length) return false;
|
|
153
|
+
try {
|
|
154
|
+
return timingSafeEqual(Buffer.from(actual), Buffer.from(expected));
|
|
155
|
+
} catch {
|
|
156
|
+
return false;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// src/api/analytics-identity-profile.ts
|
|
161
|
+
var ANALYTICS_IDENTITY_PROFILE_PURPOSE = "enhanced_matching";
|
|
162
|
+
var ANALYTICS_IDENTITY_PROFILE_RETENTION_DAYS = 90;
|
|
163
|
+
var ANALYTICS_IDENTITY_PROFILE_RATE_LIMIT = 10;
|
|
164
|
+
var AnalyticsIdentityProfileError = class extends Error {
|
|
165
|
+
constructor(code, message, status) {
|
|
166
|
+
super(message);
|
|
167
|
+
this.code = code;
|
|
168
|
+
this.status = status;
|
|
169
|
+
this.name = "AnalyticsIdentityProfileError";
|
|
170
|
+
}
|
|
171
|
+
code;
|
|
172
|
+
status;
|
|
173
|
+
};
|
|
174
|
+
function normalizedText(value, max) {
|
|
175
|
+
const normalized = value?.trim().replace(/\s+/g, " ").slice(0, max);
|
|
176
|
+
return normalized || null;
|
|
177
|
+
}
|
|
178
|
+
function maskedEmail(value) {
|
|
179
|
+
if (!value) return null;
|
|
180
|
+
const [local, domain] = value.split("@");
|
|
181
|
+
if (!local || !domain) return null;
|
|
182
|
+
return `${local.slice(0, 1)}${"*".repeat(Math.min(6, Math.max(2, local.length - 1)))}@${domain}`;
|
|
183
|
+
}
|
|
184
|
+
function maskedPhone(value) {
|
|
185
|
+
if (!value) return null;
|
|
186
|
+
const digits = value.replace(/\D/g, "");
|
|
187
|
+
if (digits.length < 4) return null;
|
|
188
|
+
return `***-***-${digits.slice(-4)}`;
|
|
189
|
+
}
|
|
190
|
+
function auditSubject(reference) {
|
|
191
|
+
return createHash("sha256").update(reference).digest("hex");
|
|
192
|
+
}
|
|
193
|
+
async function assertAnalyticsIdentitySignalsNotTombstoned(db, input) {
|
|
194
|
+
if (!input.signals.length) return;
|
|
195
|
+
const kinds = input.signals.map((signal) => signal.kind);
|
|
196
|
+
const hashes = input.signals.map((signal) => signal.valueHmac);
|
|
197
|
+
const result = await db.query(
|
|
198
|
+
`SELECT 1 FROM analytics_identity_tombstones
|
|
199
|
+
WHERE site_id=$1 AND (kind,value_hmac) IN (
|
|
200
|
+
SELECT * FROM unnest($2::text[],$3::text[])
|
|
201
|
+
) LIMIT 1`,
|
|
202
|
+
[input.siteId, kinds, hashes]
|
|
203
|
+
);
|
|
204
|
+
if (result.rowCount) {
|
|
205
|
+
throw new AnalyticsIdentityProfileError(
|
|
206
|
+
"analytics_identity_deleted",
|
|
207
|
+
"This identity cannot be automatically recreated.",
|
|
208
|
+
410
|
|
209
|
+
);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
async function writeAnalyticsConfirmedIdentityProfile(db, input) {
|
|
213
|
+
const displayName = normalizedText(input.displayName, 160);
|
|
214
|
+
const email = normalizedText(input.email, 320)?.toLowerCase() ?? null;
|
|
215
|
+
const phone = normalizedText(input.phone, 40);
|
|
216
|
+
if (!displayName && !email && !phone) return false;
|
|
217
|
+
const person = await db.query(
|
|
218
|
+
`SELECT p.public_ref FROM analytics_people p
|
|
219
|
+
JOIN analytics_identity_namespaces n ON n.id=p.namespace_id AND n.site_id=p.site_id AND n.status='approved'
|
|
220
|
+
WHERE p.id=$1 AND p.site_id=$2`,
|
|
221
|
+
[input.personId, input.siteId]
|
|
222
|
+
);
|
|
223
|
+
const publicRef = person.rows[0]?.public_ref;
|
|
224
|
+
if (!publicRef) return false;
|
|
225
|
+
const refs = [...new Set([publicRef, ...input.consentSubjectRefs].filter(Boolean))];
|
|
226
|
+
const consent = await db.query(
|
|
227
|
+
`SELECT s.latest_receipt_id::text FROM analytics_consent_current_state s
|
|
228
|
+
JOIN analytics_consent_receipts r ON r.id=s.latest_receipt_id AND r.site_id=s.site_id
|
|
229
|
+
WHERE s.site_id=$1 AND s.subject_ref=ANY($2::text[])
|
|
230
|
+
AND s.site_analytics=true AND s.enhanced_matching=true AND s.gpc=false
|
|
231
|
+
AND r.action NOT IN ('withdraw','gpc')
|
|
232
|
+
AND (s.expires_at IS NULL OR s.expires_at > now())
|
|
233
|
+
ORDER BY s.effective_at DESC LIMIT 1`,
|
|
234
|
+
[input.siteId, refs]
|
|
235
|
+
);
|
|
236
|
+
const consentReceiptId = consent.rows[0]?.latest_receipt_id;
|
|
237
|
+
if (!consentReceiptId) return false;
|
|
238
|
+
const verifiedAt = input.verifiedAt && Number.isFinite(Date.parse(input.verifiedAt)) ? new Date(input.verifiedAt).toISOString() : (/* @__PURE__ */ new Date()).toISOString();
|
|
239
|
+
const ciphertext = encryptAnalyticsRestricted(JSON.stringify({
|
|
240
|
+
schemaVersion: 1,
|
|
241
|
+
displayName,
|
|
242
|
+
email,
|
|
243
|
+
phone,
|
|
244
|
+
evidenceType: input.evidenceType,
|
|
245
|
+
verifiedAt
|
|
246
|
+
}), "identity-profile");
|
|
247
|
+
await db.query(
|
|
248
|
+
`INSERT INTO analytics_restricted_data(id,site_id,person_id,purpose,subject_ref,payload_ciphertext,expires_at,consent_receipt_id)
|
|
249
|
+
VALUES($1,$2,$3,$4,$5,$6,now()+($7::int * interval '1 day'),$8)
|
|
250
|
+
ON CONFLICT(site_id,purpose,subject_ref) DO UPDATE SET
|
|
251
|
+
person_id=EXCLUDED.person_id,payload_ciphertext=EXCLUDED.payload_ciphertext,
|
|
252
|
+
expires_at=EXCLUDED.expires_at,consent_receipt_id=EXCLUDED.consent_receipt_id,created_at=now()`,
|
|
253
|
+
[
|
|
254
|
+
randomUUID(),
|
|
255
|
+
input.siteId,
|
|
256
|
+
input.personId,
|
|
257
|
+
ANALYTICS_IDENTITY_PROFILE_PURPOSE,
|
|
258
|
+
publicRef,
|
|
259
|
+
ciphertext,
|
|
260
|
+
ANALYTICS_IDENTITY_PROFILE_RETENTION_DAYS,
|
|
261
|
+
consentReceiptId
|
|
262
|
+
]
|
|
263
|
+
);
|
|
264
|
+
return true;
|
|
265
|
+
}
|
|
266
|
+
async function resolveAnalyticsMaskedIdentityProfile(db, input) {
|
|
267
|
+
const now = input.now ?? /* @__PURE__ */ new Date();
|
|
268
|
+
const role = await db.query(
|
|
269
|
+
"SELECT role FROM analytics_site_grants WHERE site_id=$1 AND user_id=$2",
|
|
270
|
+
[input.siteId, input.actorUserId]
|
|
271
|
+
);
|
|
272
|
+
if (!role.rowCount) throw new AnalyticsIdentityProfileError("analytics_site_not_found", "Site not found.", 404);
|
|
273
|
+
if (!["owner", "editor"].includes(role.rows[0].role))
|
|
274
|
+
throw new AnalyticsIdentityProfileError("analytics_forbidden", "Editor access is required.", 403);
|
|
275
|
+
const recent = await db.query(
|
|
276
|
+
`SELECT count(*)::text AS count FROM analytics_identity_profile_access_audits
|
|
277
|
+
WHERE site_id=$1 AND actor_user_id=$2 AND created_at > $3::timestamptz - interval '1 minute'`,
|
|
278
|
+
[input.siteId, input.actorUserId, now.toISOString()]
|
|
279
|
+
);
|
|
280
|
+
if (Number(recent.rows[0]?.count ?? 0) >= ANALYTICS_IDENTITY_PROFILE_RATE_LIMIT) {
|
|
281
|
+
await db.query(
|
|
282
|
+
`INSERT INTO analytics_identity_profile_access_audits(id,site_id,actor_user_id,subject_ref_hmac,purpose,result,created_at)
|
|
283
|
+
VALUES($1,$2,$3,$4,'journey_identity_display','rate_limited',$5)`,
|
|
284
|
+
[randomUUID(), input.siteId, input.actorUserId, auditSubject(input.requestedPersonRef), now.toISOString()]
|
|
285
|
+
);
|
|
286
|
+
throw new AnalyticsIdentityProfileError("analytics_identity_profile_rate_limited", "Identity profile rate limit exceeded.", 429);
|
|
287
|
+
}
|
|
288
|
+
const unavailable = async (result) => {
|
|
289
|
+
await db.query(
|
|
290
|
+
`INSERT INTO analytics_identity_profile_access_audits(id,site_id,actor_user_id,subject_ref_hmac,purpose,result,created_at)
|
|
291
|
+
VALUES($1,$2,$3,$4,'journey_identity_display',$5,$6)`,
|
|
292
|
+
[randomUUID(), input.siteId, input.actorUserId, auditSubject(input.requestedPersonRef), result, now.toISOString()]
|
|
293
|
+
);
|
|
294
|
+
throw new AnalyticsIdentityProfileError("analytics_identity_profile_not_found", "Identity profile not found.", 404);
|
|
295
|
+
};
|
|
296
|
+
if (!input.personId) return unavailable("not_found");
|
|
297
|
+
const row = await db.query(
|
|
298
|
+
`SELECT p.public_ref,r.payload_ciphertext,r.expires_at
|
|
299
|
+
FROM analytics_people p
|
|
300
|
+
JOIN analytics_restricted_data r ON r.person_id=p.id AND r.site_id=p.site_id
|
|
301
|
+
AND r.purpose=$3 AND r.subject_ref=p.public_ref AND r.expires_at>$4
|
|
302
|
+
JOIN analytics_consent_current_state s ON s.site_id=p.site_id AND s.latest_receipt_id=r.consent_receipt_id
|
|
303
|
+
AND s.site_analytics=true AND s.enhanced_matching=true AND s.gpc=false
|
|
304
|
+
AND (s.expires_at IS NULL OR s.expires_at>$4)
|
|
305
|
+
JOIN analytics_consent_receipts c ON c.id=s.latest_receipt_id AND c.site_id=s.site_id
|
|
306
|
+
AND c.action NOT IN ('withdraw','gpc')
|
|
307
|
+
WHERE p.id=$1 AND p.site_id=$2
|
|
308
|
+
AND NOT EXISTS (SELECT 1 FROM analytics_identity_tombstones t WHERE t.site_id=p.site_id AND t.person_id=p.id)
|
|
309
|
+
LIMIT 1`,
|
|
310
|
+
[input.personId, input.siteId, ANALYTICS_IDENTITY_PROFILE_PURPOSE, now.toISOString()]
|
|
311
|
+
);
|
|
312
|
+
if (!row.rowCount) return unavailable("not_found");
|
|
313
|
+
const plaintext = decryptAnalyticsRestricted(row.rows[0].payload_ciphertext, "identity-profile");
|
|
314
|
+
if (!plaintext) return unavailable("unavailable");
|
|
315
|
+
let parsed;
|
|
316
|
+
try {
|
|
317
|
+
parsed = JSON.parse(plaintext);
|
|
318
|
+
} catch {
|
|
319
|
+
return unavailable("unavailable");
|
|
320
|
+
}
|
|
321
|
+
const evidenceType = parsed.evidenceType;
|
|
322
|
+
if (!["first_party_form", "authenticated_user", "crm_contact"].includes(String(evidenceType)))
|
|
323
|
+
return unavailable("unavailable");
|
|
324
|
+
const profile = {
|
|
325
|
+
personRef: input.requestedPersonRef,
|
|
326
|
+
displayName: normalizedText(typeof parsed.displayName === "string" ? parsed.displayName : void 0, 160),
|
|
327
|
+
maskedEmail: maskedEmail(typeof parsed.email === "string" ? parsed.email : null),
|
|
328
|
+
maskedPhone: maskedPhone(typeof parsed.phone === "string" ? parsed.phone : null),
|
|
329
|
+
evidenceType,
|
|
330
|
+
verifiedAt: typeof parsed.verifiedAt === "string" && Number.isFinite(Date.parse(parsed.verifiedAt)) ? new Date(parsed.verifiedAt).toISOString() : now.toISOString()
|
|
331
|
+
};
|
|
332
|
+
await db.query(
|
|
333
|
+
`INSERT INTO analytics_identity_profile_access_audits(id,site_id,person_id,actor_user_id,subject_ref_hmac,purpose,result,created_at)
|
|
334
|
+
VALUES($1,$2,$3,$4,$5,'journey_identity_display','returned_masked',$6)`,
|
|
335
|
+
[randomUUID(), input.siteId, input.personId, input.actorUserId, auditSubject(input.requestedPersonRef), now.toISOString()]
|
|
336
|
+
);
|
|
337
|
+
return profile;
|
|
338
|
+
}
|
|
339
|
+
|
|
85
340
|
// src/api/analytics-provider-registry.ts
|
|
86
341
|
var META_ACTION_SOURCES = /* @__PURE__ */ new Set([
|
|
87
342
|
"email",
|
|
@@ -1289,6 +1544,33 @@ async function migrateAnalytics() {
|
|
|
1289
1544
|
UNIQUE(site_id, purpose, subject_ref)
|
|
1290
1545
|
)
|
|
1291
1546
|
`);
|
|
1547
|
+
await client.query(`
|
|
1548
|
+
CREATE TABLE IF NOT EXISTS analytics_identity_tombstones (
|
|
1549
|
+
id uuid PRIMARY KEY,
|
|
1550
|
+
site_id uuid NOT NULL REFERENCES analytics_sites(id) ON DELETE CASCADE,
|
|
1551
|
+
person_id uuid,
|
|
1552
|
+
kind text NOT NULL,
|
|
1553
|
+
value_hmac text NOT NULL,
|
|
1554
|
+
reason text NOT NULL CHECK (reason IN ('subject_deletion')),
|
|
1555
|
+
created_at timestamptz NOT NULL DEFAULT now(),
|
|
1556
|
+
UNIQUE(site_id,kind,value_hmac)
|
|
1557
|
+
)
|
|
1558
|
+
`);
|
|
1559
|
+
await client.query(`
|
|
1560
|
+
CREATE TABLE IF NOT EXISTS analytics_identity_profile_access_audits (
|
|
1561
|
+
id uuid PRIMARY KEY,
|
|
1562
|
+
site_id uuid NOT NULL REFERENCES analytics_sites(id) ON DELETE CASCADE,
|
|
1563
|
+
person_id uuid,
|
|
1564
|
+
actor_user_id bigint NOT NULL,
|
|
1565
|
+
subject_ref_hmac text NOT NULL,
|
|
1566
|
+
purpose text NOT NULL CHECK (purpose IN ('journey_identity_display')),
|
|
1567
|
+
result text NOT NULL CHECK (result IN ('returned_masked','not_found','unavailable','rate_limited')),
|
|
1568
|
+
created_at timestamptz NOT NULL DEFAULT now()
|
|
1569
|
+
)
|
|
1570
|
+
`);
|
|
1571
|
+
await client.query(
|
|
1572
|
+
"CREATE INDEX IF NOT EXISTS analytics_identity_profile_access_rate ON analytics_identity_profile_access_audits(site_id,actor_user_id,created_at DESC)"
|
|
1573
|
+
);
|
|
1292
1574
|
await client.query(
|
|
1293
1575
|
`ALTER TABLE analytics_activation_destinations ADD COLUMN IF NOT EXISTS readiness text NOT NULL DEFAULT 'configured_unverified' CHECK (readiness IN ('not_configured','configured_unverified','verified','live','degraded','disabled'))`
|
|
1294
1576
|
);
|
|
@@ -1390,6 +1672,11 @@ async function migrateAnalytics() {
|
|
|
1390
1672
|
await client.query(
|
|
1391
1673
|
`ALTER TABLE analytics_people ADD COLUMN IF NOT EXISTS public_ref text`
|
|
1392
1674
|
);
|
|
1675
|
+
await client.query(
|
|
1676
|
+
`UPDATE analytics_people
|
|
1677
|
+
SET public_ref = 'xrp_' || substr(md5(random()::text || clock_timestamp()::text || id::text || site_id::text), 1, 24)
|
|
1678
|
+
WHERE public_ref IS NULL`
|
|
1679
|
+
);
|
|
1393
1680
|
await client.query(
|
|
1394
1681
|
`ALTER TABLE analytics_events ADD COLUMN IF NOT EXISTS event_definition_id uuid REFERENCES analytics_event_definitions(id) ON DELETE SET NULL`
|
|
1395
1682
|
);
|
|
@@ -1554,6 +1841,9 @@ async function migrateAnalytics() {
|
|
|
1554
1841
|
PRIMARY KEY(site_id, subject_ref)
|
|
1555
1842
|
)
|
|
1556
1843
|
`);
|
|
1844
|
+
await client.query(
|
|
1845
|
+
"ALTER TABLE analytics_restricted_data ADD COLUMN IF NOT EXISTS consent_receipt_id uuid REFERENCES analytics_consent_receipts(id) ON DELETE CASCADE"
|
|
1846
|
+
);
|
|
1557
1847
|
await client.query(`
|
|
1558
1848
|
CREATE TABLE IF NOT EXISTS analytics_identity_namespaces (
|
|
1559
1849
|
id uuid PRIMARY KEY,
|
|
@@ -1586,13 +1876,19 @@ async function migrateAnalytics() {
|
|
|
1586
1876
|
)
|
|
1587
1877
|
`);
|
|
1588
1878
|
await client.query(
|
|
1589
|
-
`ALTER TABLE analytics_people ADD COLUMN IF NOT EXISTS namespace_id uuid
|
|
1879
|
+
`ALTER TABLE analytics_people ADD COLUMN IF NOT EXISTS namespace_id uuid`
|
|
1590
1880
|
);
|
|
1591
1881
|
await client.query(
|
|
1592
|
-
`ALTER TABLE analytics_identity_nodes ADD COLUMN IF NOT EXISTS namespace_id uuid
|
|
1882
|
+
`ALTER TABLE analytics_identity_nodes ADD COLUMN IF NOT EXISTS namespace_id uuid`
|
|
1593
1883
|
);
|
|
1594
1884
|
await client.query(
|
|
1595
|
-
`ALTER TABLE analytics_identity_edges ADD COLUMN IF NOT EXISTS namespace_id uuid
|
|
1885
|
+
`ALTER TABLE analytics_identity_edges ADD COLUMN IF NOT EXISTS namespace_id uuid`
|
|
1886
|
+
);
|
|
1887
|
+
await client.query(
|
|
1888
|
+
"CREATE UNIQUE INDEX IF NOT EXISTS analytics_people_site_ref ON analytics_people(id, site_id)"
|
|
1889
|
+
);
|
|
1890
|
+
await client.query(
|
|
1891
|
+
"CREATE UNIQUE INDEX IF NOT EXISTS analytics_identity_nodes_site_ref ON analytics_identity_nodes(id, site_id)"
|
|
1596
1892
|
);
|
|
1597
1893
|
await client.query(
|
|
1598
1894
|
"CREATE UNIQUE INDEX IF NOT EXISTS analytics_identity_nodes_namespace_ref ON analytics_identity_nodes(id, site_id, namespace_id)"
|
|
@@ -1600,6 +1896,62 @@ async function migrateAnalytics() {
|
|
|
1600
1896
|
await client.query(
|
|
1601
1897
|
"CREATE UNIQUE INDEX IF NOT EXISTS analytics_people_namespace_ref ON analytics_people(id, site_id, namespace_id)"
|
|
1602
1898
|
);
|
|
1899
|
+
await client.query(`
|
|
1900
|
+
DO $$
|
|
1901
|
+
BEGIN
|
|
1902
|
+
IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname='analytics_people_namespace_site_fkey') THEN
|
|
1903
|
+
ALTER TABLE analytics_people ADD CONSTRAINT analytics_people_namespace_site_fkey
|
|
1904
|
+
FOREIGN KEY(namespace_id, site_id) REFERENCES analytics_identity_namespaces(id, site_id)
|
|
1905
|
+
ON DELETE RESTRICT NOT VALID;
|
|
1906
|
+
END IF;
|
|
1907
|
+
IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname='analytics_identity_nodes_namespace_site_fkey') THEN
|
|
1908
|
+
ALTER TABLE analytics_identity_nodes ADD CONSTRAINT analytics_identity_nodes_namespace_site_fkey
|
|
1909
|
+
FOREIGN KEY(namespace_id, site_id) REFERENCES analytics_identity_namespaces(id, site_id)
|
|
1910
|
+
ON DELETE RESTRICT NOT VALID;
|
|
1911
|
+
END IF;
|
|
1912
|
+
IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname='analytics_identity_edges_person_site_fkey') THEN
|
|
1913
|
+
ALTER TABLE analytics_identity_edges ADD CONSTRAINT analytics_identity_edges_person_site_fkey
|
|
1914
|
+
FOREIGN KEY(person_id, site_id) REFERENCES analytics_people(id, site_id)
|
|
1915
|
+
ON DELETE CASCADE NOT VALID;
|
|
1916
|
+
END IF;
|
|
1917
|
+
IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname='analytics_identity_edges_node_site_fkey') THEN
|
|
1918
|
+
ALTER TABLE analytics_identity_edges ADD CONSTRAINT analytics_identity_edges_node_site_fkey
|
|
1919
|
+
FOREIGN KEY(identity_node_id, site_id) REFERENCES analytics_identity_nodes(id, site_id)
|
|
1920
|
+
ON DELETE CASCADE NOT VALID;
|
|
1921
|
+
END IF;
|
|
1922
|
+
IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname='analytics_identity_edges_namespace_site_fkey') THEN
|
|
1923
|
+
ALTER TABLE analytics_identity_edges ADD CONSTRAINT analytics_identity_edges_namespace_site_fkey
|
|
1924
|
+
FOREIGN KEY(namespace_id, site_id) REFERENCES analytics_identity_namespaces(id, site_id)
|
|
1925
|
+
ON DELETE RESTRICT NOT VALID;
|
|
1926
|
+
END IF;
|
|
1927
|
+
IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname='analytics_identity_edges_person_namespace_fkey') THEN
|
|
1928
|
+
ALTER TABLE analytics_identity_edges ADD CONSTRAINT analytics_identity_edges_person_namespace_fkey
|
|
1929
|
+
FOREIGN KEY(person_id, site_id, namespace_id) REFERENCES analytics_people(id, site_id, namespace_id)
|
|
1930
|
+
ON DELETE CASCADE NOT VALID;
|
|
1931
|
+
END IF;
|
|
1932
|
+
IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname='analytics_identity_edges_node_namespace_fkey') THEN
|
|
1933
|
+
ALTER TABLE analytics_identity_edges ADD CONSTRAINT analytics_identity_edges_node_namespace_fkey
|
|
1934
|
+
FOREIGN KEY(identity_node_id, site_id, namespace_id) REFERENCES analytics_identity_nodes(id, site_id, namespace_id)
|
|
1935
|
+
ON DELETE CASCADE NOT VALID;
|
|
1936
|
+
END IF;
|
|
1937
|
+
END $$
|
|
1938
|
+
`);
|
|
1939
|
+
for (const constraint of [
|
|
1940
|
+
["analytics_people", "analytics_people_namespace_site_fkey"],
|
|
1941
|
+
["analytics_identity_nodes", "analytics_identity_nodes_namespace_site_fkey"],
|
|
1942
|
+
["analytics_identity_edges", "analytics_identity_edges_person_site_fkey"],
|
|
1943
|
+
["analytics_identity_edges", "analytics_identity_edges_node_site_fkey"],
|
|
1944
|
+
["analytics_identity_edges", "analytics_identity_edges_namespace_site_fkey"],
|
|
1945
|
+
["analytics_identity_edges", "analytics_identity_edges_person_namespace_fkey"],
|
|
1946
|
+
["analytics_identity_edges", "analytics_identity_edges_node_namespace_fkey"]
|
|
1947
|
+
]) {
|
|
1948
|
+
await client.query(`ALTER TABLE ${constraint[0]} VALIDATE CONSTRAINT ${constraint[1]}`);
|
|
1949
|
+
}
|
|
1950
|
+
await client.query("ALTER TABLE analytics_people DROP CONSTRAINT IF EXISTS analytics_people_namespace_id_fkey");
|
|
1951
|
+
await client.query("ALTER TABLE analytics_identity_nodes DROP CONSTRAINT IF EXISTS analytics_identity_nodes_namespace_id_fkey");
|
|
1952
|
+
await client.query("ALTER TABLE analytics_identity_edges DROP CONSTRAINT IF EXISTS analytics_identity_edges_namespace_id_fkey");
|
|
1953
|
+
await client.query("ALTER TABLE analytics_identity_edges DROP CONSTRAINT IF EXISTS analytics_identity_edges_person_id_fkey");
|
|
1954
|
+
await client.query("ALTER TABLE analytics_identity_edges DROP CONSTRAINT IF EXISTS analytics_identity_edges_identity_node_id_fkey");
|
|
1603
1955
|
await client.query(`
|
|
1604
1956
|
CREATE TABLE IF NOT EXISTS analytics_candidate_associations (
|
|
1605
1957
|
id uuid PRIMARY KEY,
|
|
@@ -2265,7 +2617,7 @@ async function migrateAnalytics() {
|
|
|
2265
2617
|
}
|
|
2266
2618
|
function normalizeSlug(value) {
|
|
2267
2619
|
const slug = value.trim().toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 80);
|
|
2268
|
-
return slug || `site-${
|
|
2620
|
+
return slug || `site-${randomBytes2(4).toString("hex")}`;
|
|
2269
2621
|
}
|
|
2270
2622
|
function normalizeObservedHostname(origin) {
|
|
2271
2623
|
if (!origin || origin === "null") return null;
|
|
@@ -2278,7 +2630,7 @@ function normalizeObservedHostname(origin) {
|
|
|
2278
2630
|
}
|
|
2279
2631
|
}
|
|
2280
2632
|
function publicPixelId() {
|
|
2281
|
-
return `px_${
|
|
2633
|
+
return `px_${randomBytes2(18).toString("base64url")}`;
|
|
2282
2634
|
}
|
|
2283
2635
|
function mapRows(rows) {
|
|
2284
2636
|
return rows;
|
|
@@ -2316,7 +2668,7 @@ async function requireAnalyticsEditor(client, siteId, userId) {
|
|
|
2316
2668
|
async function createAnalyticsSite(input) {
|
|
2317
2669
|
const db = getAnalyticsPool();
|
|
2318
2670
|
const client = await db.connect();
|
|
2319
|
-
const id =
|
|
2671
|
+
const id = randomUUID2();
|
|
2320
2672
|
const baseSlug = normalizeSlug(input.slug || input.name);
|
|
2321
2673
|
try {
|
|
2322
2674
|
await client.query("BEGIN");
|
|
@@ -2332,7 +2684,7 @@ async function createAnalyticsSite(input) {
|
|
|
2332
2684
|
} catch (error) {
|
|
2333
2685
|
const code = error.code;
|
|
2334
2686
|
if (code !== "23505" || attempt === 2) throw error;
|
|
2335
|
-
slug = `${baseSlug}-${
|
|
2687
|
+
slug = `${baseSlug}-${randomBytes2(2).toString("hex")}`;
|
|
2336
2688
|
}
|
|
2337
2689
|
}
|
|
2338
2690
|
await client.query(
|
|
@@ -2547,7 +2899,7 @@ async function createAnalyticsPixel(input) {
|
|
|
2547
2899
|
VALUES ($1, $2, $3, $4, $5)
|
|
2548
2900
|
RETURNING id, site_id, public_id, name, environment, status, created_at::text, NULL::text AS last_event_at`,
|
|
2549
2901
|
[
|
|
2550
|
-
|
|
2902
|
+
randomUUID2(),
|
|
2551
2903
|
input.siteId,
|
|
2552
2904
|
publicPixelId(),
|
|
2553
2905
|
input.name.trim(),
|
|
@@ -2624,7 +2976,7 @@ async function setAnalyticsPixelDomainState(input) {
|
|
|
2624
2976
|
SELECT $1, p.id, $4, $5 FROM analytics_pixels p WHERE p.id = $2 AND p.site_id = $3
|
|
2625
2977
|
ON CONFLICT(pixel_id, hostname) DO UPDATE SET state = EXCLUDED.state, updated_at = now()
|
|
2626
2978
|
RETURNING id`,
|
|
2627
|
-
[
|
|
2979
|
+
[randomUUID2(), input.pixelId, input.siteId, hostname, input.state]
|
|
2628
2980
|
);
|
|
2629
2981
|
if (!result.rowCount)
|
|
2630
2982
|
throw new AnalyticsRepositoryError(
|
|
@@ -2666,7 +3018,7 @@ async function upsertAnalyticsHostGroup(input) {
|
|
|
2666
3018
|
ON CONFLICT(pixel_id, name) DO UPDATE SET
|
|
2667
3019
|
auto_decorate=EXCLUDED.auto_decorate, updated_at=now()
|
|
2668
3020
|
RETURNING id`,
|
|
2669
|
-
[
|
|
3021
|
+
[randomUUID2(), input.siteId, input.pixelId, input.name.trim().slice(0, 120), Boolean(input.autoDecorate)]
|
|
2670
3022
|
);
|
|
2671
3023
|
await client.query("DELETE FROM analytics_host_group_members WHERE group_id=$1", [group.rows[0].id]);
|
|
2672
3024
|
for (const hostname of hostnames)
|
|
@@ -2749,7 +3101,7 @@ async function prepareAnalyticsLinkerIssue(input) {
|
|
|
2749
3101
|
VALUES ($1,$2,'visitor_id',$3)
|
|
2750
3102
|
ON CONFLICT(site_id, kind, value_hmac) DO UPDATE SET last_seen_at=now()
|
|
2751
3103
|
RETURNING id`,
|
|
2752
|
-
[
|
|
3104
|
+
[randomUUID2(), siteId, identityHmac("visitor_id", input.visitorId)]
|
|
2753
3105
|
);
|
|
2754
3106
|
const existingPerson = await client.query(
|
|
2755
3107
|
`SELECT person_id FROM analytics_identity_edges
|
|
@@ -2760,19 +3112,20 @@ async function prepareAnalyticsLinkerIssue(input) {
|
|
|
2760
3112
|
let personId = existingPerson.rows[0]?.person_id;
|
|
2761
3113
|
if (!personId) {
|
|
2762
3114
|
const person = await client.query(
|
|
2763
|
-
`INSERT INTO analytics_people(id, site_id, crm_person_ref) VALUES ($1,$2,$3)
|
|
2764
|
-
ON CONFLICT(site_id, crm_person_ref) DO UPDATE SET last_seen_at=now()
|
|
2765
|
-
|
|
3115
|
+
`INSERT INTO analytics_people(id, site_id, crm_person_ref, public_ref) VALUES ($1,$2,$3,$4)
|
|
3116
|
+
ON CONFLICT(site_id, crm_person_ref) DO UPDATE SET last_seen_at=now(),
|
|
3117
|
+
public_ref=COALESCE(analytics_people.public_ref,EXCLUDED.public_ref) RETURNING id`,
|
|
3118
|
+
[randomUUID2(), siteId, `XRay::anonymous::${identityHmac("visitor_id", input.visitorId).slice(0, 24)}`, newAnalyticsPersonPublicRef()]
|
|
2766
3119
|
);
|
|
2767
3120
|
personId = person.rows[0].id;
|
|
2768
3121
|
await client.query(
|
|
2769
3122
|
`INSERT INTO analytics_identity_edges(id, site_id, person_id, identity_node_id, evidence_kind, confidence)
|
|
2770
3123
|
VALUES ($1,$2,$3,$4,'first_party_linker',0.980)
|
|
2771
3124
|
ON CONFLICT(person_id, identity_node_id, evidence_kind) DO UPDATE SET last_seen_at=now()`,
|
|
2772
|
-
[
|
|
3125
|
+
[randomUUID2(), siteId, personId, node.rows[0].id]
|
|
2773
3126
|
);
|
|
2774
3127
|
}
|
|
2775
|
-
const tokenId =
|
|
3128
|
+
const tokenId = randomUUID2();
|
|
2776
3129
|
await client.query(
|
|
2777
3130
|
`INSERT INTO analytics_linker_tokens(
|
|
2778
3131
|
id, site_id, pixel_id, host_group_id, identity_node_id,
|
|
@@ -2828,13 +3181,13 @@ async function redeemAnalyticsLinkerRecord(input) {
|
|
|
2828
3181
|
VALUES ($1,$2,'visitor_id',$3)
|
|
2829
3182
|
ON CONFLICT(site_id, kind, value_hmac) DO UPDATE SET last_seen_at=now()
|
|
2830
3183
|
RETURNING id`,
|
|
2831
|
-
[
|
|
3184
|
+
[randomUUID2(), input.siteId, identityHmac("visitor_id", input.visitorId)]
|
|
2832
3185
|
);
|
|
2833
3186
|
await client.query(
|
|
2834
3187
|
`INSERT INTO analytics_identity_edges(id, site_id, person_id, identity_node_id, evidence_kind, confidence)
|
|
2835
3188
|
VALUES ($1,$2,$3,$4,'cross_domain_linker',0.980)
|
|
2836
3189
|
ON CONFLICT(person_id, identity_node_id, evidence_kind) DO UPDATE SET last_seen_at=now()`,
|
|
2837
|
-
[
|
|
3190
|
+
[randomUUID2(), input.siteId, personId, targetNode.rows[0].id]
|
|
2838
3191
|
);
|
|
2839
3192
|
await client.query(
|
|
2840
3193
|
"UPDATE analytics_events SET person_id=$1 WHERE site_id=$2 AND visitor_id=$3 AND person_id IS NULL",
|
|
@@ -2858,6 +3211,133 @@ async function redeemAnalyticsLinkerRecord(input) {
|
|
|
2858
3211
|
client.release();
|
|
2859
3212
|
}
|
|
2860
3213
|
}
|
|
3214
|
+
function analyticsIdentityResolutionAllowed(event) {
|
|
3215
|
+
return Boolean(
|
|
3216
|
+
event.consent?.analytics === "granted" && (event.consent.purposes?.enhanced_matching === "granted" || event.consent.adUserData === "granted") && event.consent.globalPrivacyControl !== true
|
|
3217
|
+
);
|
|
3218
|
+
}
|
|
3219
|
+
function analyticsIdentityPromotionAllowed(event) {
|
|
3220
|
+
return Boolean(event.identity && analyticsIdentityResolutionAllowed(event));
|
|
3221
|
+
}
|
|
3222
|
+
async function projectAnalyticsPixelEventConsent(client, input) {
|
|
3223
|
+
const visitorId = input.event.visitorId?.trim().slice(0, 240);
|
|
3224
|
+
const consent = input.event.consent;
|
|
3225
|
+
if (!visitorId || !consent) return null;
|
|
3226
|
+
const occurredAtMs = Date.parse(input.event.occurredAt);
|
|
3227
|
+
const serverNow = Date.now();
|
|
3228
|
+
if (!Number.isFinite(occurredAtMs) || occurredAtMs > serverNow + 5 * 6e4 || occurredAtMs < serverNow - 24 * 60 * 6e4)
|
|
3229
|
+
return null;
|
|
3230
|
+
const explicit = consent.purposes;
|
|
3231
|
+
const purposes = {
|
|
3232
|
+
siteAnalytics: explicit ? explicit.site_analytics === "granted" : consent.analytics === "granted",
|
|
3233
|
+
enhancedMatching: explicit ? explicit.enhanced_matching === "granted" : consent.adUserData === "granted",
|
|
3234
|
+
advertisingMeasurement: explicit ? explicit.advertising_measurement === "granted" : consent.adPersonalization === "granted"
|
|
3235
|
+
};
|
|
3236
|
+
const gpc = consent.globalPrivacyControl === true;
|
|
3237
|
+
if (gpc) {
|
|
3238
|
+
purposes.siteAnalytics = false;
|
|
3239
|
+
purposes.enhancedMatching = false;
|
|
3240
|
+
purposes.advertisingMeasurement = false;
|
|
3241
|
+
}
|
|
3242
|
+
const action = gpc ? "gpc" : Object.values(purposes).every(Boolean) ? "accept_all" : Object.values(purposes).every((value) => !value) ? "reject_all" : "save_choices";
|
|
3243
|
+
const occurredAt = new Date(occurredAtMs).toISOString();
|
|
3244
|
+
const previous = await client.query(
|
|
3245
|
+
`SELECT s.latest_receipt_id::text,s.site_analytics,s.enhanced_matching,s.advertising_measurement,s.gpc,r.action
|
|
3246
|
+
FROM analytics_consent_current_state s JOIN analytics_consent_receipts r ON r.id=s.latest_receipt_id
|
|
3247
|
+
WHERE s.site_id=$1 AND s.subject_ref=$2 FOR UPDATE`,
|
|
3248
|
+
[input.siteId, visitorId]
|
|
3249
|
+
);
|
|
3250
|
+
const prior = previous.rows[0];
|
|
3251
|
+
if (prior && prior.site_analytics === purposes.siteAnalytics && prior.enhanced_matching === purposes.enhancedMatching && prior.advertising_measurement === purposes.advertisingMeasurement && prior.gpc === gpc && prior.action === action) return prior.latest_receipt_id;
|
|
3252
|
+
const receiptId = randomUUID2();
|
|
3253
|
+
await client.query(
|
|
3254
|
+
`INSERT INTO analytics_consent_receipts(id,site_id,pixel_id,subject_ref,policy_version,jurisdiction_profile,
|
|
3255
|
+
necessary,site_analytics,enhanced_matching,advertising_measurement,gpc,action,source,presented_at,occurred_at,supersedes_id)
|
|
3256
|
+
VALUES($1,$2,$3,$4,1,'pixel',true,$5,$6,$7,$8,$9,'gateway',$10,$10,$11)`,
|
|
3257
|
+
[
|
|
3258
|
+
receiptId,
|
|
3259
|
+
input.siteId,
|
|
3260
|
+
input.pixelId,
|
|
3261
|
+
visitorId,
|
|
3262
|
+
purposes.siteAnalytics,
|
|
3263
|
+
purposes.enhancedMatching,
|
|
3264
|
+
purposes.advertisingMeasurement,
|
|
3265
|
+
gpc,
|
|
3266
|
+
action,
|
|
3267
|
+
occurredAt,
|
|
3268
|
+
prior?.latest_receipt_id ?? null
|
|
3269
|
+
]
|
|
3270
|
+
);
|
|
3271
|
+
await client.query(
|
|
3272
|
+
`INSERT INTO analytics_consent_current_state(site_id,subject_ref,latest_receipt_id,policy_version,jurisdiction_profile,
|
|
3273
|
+
necessary,site_analytics,enhanced_matching,advertising_measurement,gpc,effective_at)
|
|
3274
|
+
VALUES($1,$2,$3,1,'pixel',true,$4,$5,$6,$7,$8)
|
|
3275
|
+
ON CONFLICT(site_id,subject_ref) DO UPDATE SET latest_receipt_id=EXCLUDED.latest_receipt_id,
|
|
3276
|
+
policy_version=EXCLUDED.policy_version,jurisdiction_profile=EXCLUDED.jurisdiction_profile,
|
|
3277
|
+
site_analytics=EXCLUDED.site_analytics,enhanced_matching=EXCLUDED.enhanced_matching,
|
|
3278
|
+
advertising_measurement=EXCLUDED.advertising_measurement,gpc=EXCLUDED.gpc,
|
|
3279
|
+
effective_at=EXCLUDED.effective_at,updated_at=now()
|
|
3280
|
+
WHERE EXCLUDED.effective_at>=analytics_consent_current_state.effective_at`,
|
|
3281
|
+
[
|
|
3282
|
+
input.siteId,
|
|
3283
|
+
visitorId,
|
|
3284
|
+
receiptId,
|
|
3285
|
+
purposes.siteAnalytics,
|
|
3286
|
+
purposes.enhancedMatching,
|
|
3287
|
+
purposes.advertisingMeasurement,
|
|
3288
|
+
gpc,
|
|
3289
|
+
occurredAt
|
|
3290
|
+
]
|
|
3291
|
+
);
|
|
3292
|
+
return receiptId;
|
|
3293
|
+
}
|
|
3294
|
+
async function resolveAnalyticsConfirmedActivationIdentity(db, input) {
|
|
3295
|
+
if (!input.subjectRef) return { personId: null, activationEligible: false };
|
|
3296
|
+
const result = await db.query(
|
|
3297
|
+
`SELECT p.id::text AS person_id,s.advertising_measurement
|
|
3298
|
+
FROM analytics_people p
|
|
3299
|
+
JOIN analytics_consent_current_state s ON s.site_id=p.site_id AND s.subject_ref=$2
|
|
3300
|
+
JOIN analytics_consent_receipts r ON r.id=s.latest_receipt_id AND r.site_id=s.site_id
|
|
3301
|
+
WHERE p.site_id=$1
|
|
3302
|
+
AND (($3::uuid IS NOT NULL AND p.id=$3) OR ($4::text IS NOT NULL AND EXISTS (
|
|
3303
|
+
SELECT 1 FROM analytics_events e WHERE e.site_id=p.site_id AND e.event_id=$4
|
|
3304
|
+
AND e.person_id=p.id AND e.visitor_id=$2
|
|
3305
|
+
)))
|
|
3306
|
+
AND s.site_analytics=true AND s.gpc=false AND r.action NOT IN ('withdraw','gpc')
|
|
3307
|
+
AND (s.expires_at IS NULL OR s.expires_at>now()) LIMIT 1`,
|
|
3308
|
+
[input.siteId, input.subjectRef, input.personId ?? null, input.eventId ?? null]
|
|
3309
|
+
);
|
|
3310
|
+
const row = result.rows[0];
|
|
3311
|
+
return row ? { personId: row.person_id, activationEligible: row.advertising_measurement === true } : { personId: null, activationEligible: false };
|
|
3312
|
+
}
|
|
3313
|
+
async function hasCurrentAnalyticsIdentityConsent(client, siteId, visitorId, sessionId) {
|
|
3314
|
+
if (!visitorId?.trim() || !sessionId?.trim()) return false;
|
|
3315
|
+
const result = await client.query(
|
|
3316
|
+
`SELECT 1 FROM analytics_consent_current_state s
|
|
3317
|
+
JOIN analytics_consent_receipts r ON r.id=s.latest_receipt_id AND r.site_id=s.site_id
|
|
3318
|
+
WHERE s.site_id=$1 AND s.subject_ref=$2
|
|
3319
|
+
AND s.site_analytics=true AND s.enhanced_matching=true AND s.gpc=false
|
|
3320
|
+
AND r.action NOT IN ('withdraw','gpc')
|
|
3321
|
+
AND (s.expires_at IS NULL OR s.expires_at>now())
|
|
3322
|
+
AND EXISTS (SELECT 1 FROM analytics_sessions x
|
|
3323
|
+
WHERE x.site_id=s.site_id AND x.visitor_hmac=$3 AND x.session_hmac=$4)
|
|
3324
|
+
ORDER BY s.effective_at DESC LIMIT 1`,
|
|
3325
|
+
[siteId, visitorId, identityHmac("visitor_id", visitorId), identityHmac("session_id", sessionId)]
|
|
3326
|
+
);
|
|
3327
|
+
return Boolean(result.rowCount);
|
|
3328
|
+
}
|
|
3329
|
+
async function hasCurrentPersonIdentityConsent(client, siteId, personPublicRef) {
|
|
3330
|
+
const result = await client.query(
|
|
3331
|
+
`SELECT 1 FROM analytics_consent_current_state s
|
|
3332
|
+
JOIN analytics_consent_receipts r ON r.id=s.latest_receipt_id AND r.site_id=s.site_id
|
|
3333
|
+
WHERE s.site_id=$1 AND s.subject_ref=$2
|
|
3334
|
+
AND s.site_analytics=true AND s.enhanced_matching=true AND s.gpc=false
|
|
3335
|
+
AND r.action NOT IN ('withdraw','gpc')
|
|
3336
|
+
AND (s.expires_at IS NULL OR s.expires_at>now()) LIMIT 1`,
|
|
3337
|
+
[siteId, personPublicRef]
|
|
3338
|
+
);
|
|
3339
|
+
return Boolean(result.rowCount);
|
|
3340
|
+
}
|
|
2861
3341
|
var MAX_ENGAGED_MS = 30 * 60 * 1e3;
|
|
2862
3342
|
var ENGAGED_SESSION_MS = 1e4;
|
|
2863
3343
|
function boundedEngagedMs(value) {
|
|
@@ -2926,7 +3406,7 @@ async function materializeAnalyticsSessionAndTouch(input) {
|
|
|
2926
3406
|
updated_at = now()
|
|
2927
3407
|
RETURNING id`,
|
|
2928
3408
|
[
|
|
2929
|
-
|
|
3409
|
+
randomUUID2(),
|
|
2930
3410
|
input.siteId,
|
|
2931
3411
|
input.pixelId,
|
|
2932
3412
|
visitorHmac,
|
|
@@ -2942,7 +3422,7 @@ async function materializeAnalyticsSessionAndTouch(input) {
|
|
|
2942
3422
|
input.event.source || input.event.medium || input.event.campaign || input.event.referrer || Object.keys(clickIds).length
|
|
2943
3423
|
);
|
|
2944
3424
|
if (!hasTouch) return;
|
|
2945
|
-
const touchHash =
|
|
3425
|
+
const touchHash = createHash2("sha256").update(
|
|
2946
3426
|
JSON.stringify({
|
|
2947
3427
|
siteId: input.siteId,
|
|
2948
3428
|
sessionHmac,
|
|
@@ -2963,7 +3443,7 @@ async function materializeAnalyticsSessionAndTouch(input) {
|
|
|
2963
3443
|
person_id = COALESCE(analytics_attribution_touches.person_id, EXCLUDED.person_id)
|
|
2964
3444
|
RETURNING id`,
|
|
2965
3445
|
[
|
|
2966
|
-
|
|
3446
|
+
randomUUID2(),
|
|
2967
3447
|
input.siteId,
|
|
2968
3448
|
session.rows[0].id,
|
|
2969
3449
|
input.personId ?? null,
|
|
@@ -2987,7 +3467,7 @@ async function materializeAnalyticsSessionAndTouch(input) {
|
|
|
2987
3467
|
}
|
|
2988
3468
|
async function ingestAnalyticsEvents(input) {
|
|
2989
3469
|
const db = getAnalyticsPool();
|
|
2990
|
-
const requestId = `air_${
|
|
3470
|
+
const requestId = `air_${randomUUID2()}`;
|
|
2991
3471
|
const hostname = normalizeObservedHostname(input.origin);
|
|
2992
3472
|
const pixelResult = await db.query(
|
|
2993
3473
|
`SELECT p.id, p.site_id, p.status,
|
|
@@ -3008,7 +3488,7 @@ async function ingestAnalyticsEvents(input) {
|
|
|
3008
3488
|
`INSERT INTO analytics_ingestion_receipts(id, request_id, site_id, pixel_id, hostname, rejected_count, reason_codes)
|
|
3009
3489
|
VALUES ($1, $2, $3, $4, $5, $6, $7::jsonb)`,
|
|
3010
3490
|
[
|
|
3011
|
-
|
|
3491
|
+
randomUUID2(),
|
|
3012
3492
|
requestId,
|
|
3013
3493
|
pixel?.site_id ?? null,
|
|
3014
3494
|
pixel?.id ?? null,
|
|
@@ -3033,7 +3513,7 @@ async function ingestAnalyticsEvents(input) {
|
|
|
3033
3513
|
`INSERT INTO analytics_ingestion_receipts(id, request_id, site_id, pixel_id, hostname, rejected_count, reason_codes)
|
|
3034
3514
|
VALUES ($1, $2, $3, $4, $5, $6, $7::jsonb)`,
|
|
3035
3515
|
[
|
|
3036
|
-
|
|
3516
|
+
randomUUID2(),
|
|
3037
3517
|
requestId,
|
|
3038
3518
|
pixel.site_id,
|
|
3039
3519
|
pixel.id,
|
|
@@ -3058,7 +3538,7 @@ async function ingestAnalyticsEvents(input) {
|
|
|
3058
3538
|
`INSERT INTO analytics_ingestion_receipts(id, request_id, site_id, pixel_id, rejected_count, reason_codes)
|
|
3059
3539
|
VALUES ($1, $2, $3, $4, $5, $6::jsonb)`,
|
|
3060
3540
|
[
|
|
3061
|
-
|
|
3541
|
+
randomUUID2(),
|
|
3062
3542
|
requestId,
|
|
3063
3543
|
pixel.site_id,
|
|
3064
3544
|
pixel.id,
|
|
@@ -3082,7 +3562,7 @@ async function ingestAnalyticsEvents(input) {
|
|
|
3082
3562
|
ON CONFLICT(pixel_id, hostname) DO UPDATE
|
|
3083
3563
|
SET last_seen_at = now(), updated_at = now()
|
|
3084
3564
|
RETURNING state`,
|
|
3085
|
-
[
|
|
3565
|
+
[randomUUID2(), pixel.id, hostname]
|
|
3086
3566
|
);
|
|
3087
3567
|
if (domainResult.rows[0]?.state !== "approved") {
|
|
3088
3568
|
await db.query(
|
|
@@ -3096,7 +3576,7 @@ async function ingestAnalyticsEvents(input) {
|
|
|
3096
3576
|
`INSERT INTO analytics_ingestion_receipts(id, request_id, site_id, pixel_id, hostname, rejected_count, reason_codes)
|
|
3097
3577
|
VALUES ($1, $2, $3, $4, $5, $6, $7::jsonb)`,
|
|
3098
3578
|
[
|
|
3099
|
-
|
|
3579
|
+
randomUUID2(),
|
|
3100
3580
|
requestId,
|
|
3101
3581
|
pixel.site_id,
|
|
3102
3582
|
pixel.id,
|
|
@@ -3123,18 +3603,14 @@ async function ingestAnalyticsEvents(input) {
|
|
|
3123
3603
|
await client.query("BEGIN");
|
|
3124
3604
|
for (const event of input.events) {
|
|
3125
3605
|
try {
|
|
3126
|
-
|
|
3127
|
-
const identifiedPersonId = event.identity && event.consent?.analytics === "granted" ? await linkAnalyticsIdentityInTransaction(client, {
|
|
3606
|
+
await projectAnalyticsPixelEventConsent(client, {
|
|
3128
3607
|
siteId: pixel.site_id,
|
|
3129
|
-
|
|
3130
|
-
|
|
3131
|
-
|
|
3132
|
-
|
|
3133
|
-
|
|
3134
|
-
|
|
3135
|
-
clickIds: event.clickIds
|
|
3136
|
-
}) : null;
|
|
3137
|
-
const resolvedPerson = identifiedPersonId ? { rows: [{ person_id: identifiedPersonId }] } : event.visitorId || event.sessionId ? await client.query(
|
|
3608
|
+
pixelId: pixel.id,
|
|
3609
|
+
event
|
|
3610
|
+
});
|
|
3611
|
+
const attribution = resolveAnalyticsAttribution(event);
|
|
3612
|
+
const eventIdentity = event.identity;
|
|
3613
|
+
const resolvedPerson = analyticsIdentityResolutionAllowed(event) && (event.visitorId || event.sessionId) ? await client.query(
|
|
3138
3614
|
`SELECT e.person_id FROM analytics_identity_nodes n JOIN analytics_identity_edges e ON e.identity_node_id = n.id
|
|
3139
3615
|
WHERE n.site_id = $1 AND ((n.kind = 'visitor_id' AND n.value_hmac = $2) OR (n.kind = 'session_id' AND n.value_hmac = $3))
|
|
3140
3616
|
ORDER BY e.confidence DESC, e.last_seen_at DESC LIMIT 1`,
|
|
@@ -3156,7 +3632,7 @@ async function ingestAnalyticsEvents(input) {
|
|
|
3156
3632
|
$23, $24, $25, $26, $27, $28, $29, $30, $31, $32, $33
|
|
3157
3633
|
) ON CONFLICT(site_id, event_id) DO NOTHING`,
|
|
3158
3634
|
[
|
|
3159
|
-
|
|
3635
|
+
randomUUID2(),
|
|
3160
3636
|
event.eventId,
|
|
3161
3637
|
pixel.site_id,
|
|
3162
3638
|
pixel.id,
|
|
@@ -3193,13 +3669,34 @@ async function ingestAnalyticsEvents(input) {
|
|
|
3193
3669
|
);
|
|
3194
3670
|
if (inserted.rowCount) {
|
|
3195
3671
|
accepted += 1;
|
|
3672
|
+
const resolvedPersonId = resolvedPerson.rows[0]?.person_id;
|
|
3196
3673
|
await materializeAnalyticsSessionAndTouch({
|
|
3197
3674
|
client,
|
|
3198
3675
|
siteId: pixel.site_id,
|
|
3199
3676
|
pixelId: pixel.id,
|
|
3200
|
-
personId:
|
|
3677
|
+
personId: resolvedPersonId,
|
|
3201
3678
|
event
|
|
3202
3679
|
});
|
|
3680
|
+
if (resolvedPersonId && event.eventName === "identify") {
|
|
3681
|
+
await backfillAnalyticsConfirmedHistory(client, {
|
|
3682
|
+
siteId: pixel.site_id,
|
|
3683
|
+
personId: resolvedPersonId,
|
|
3684
|
+
visitorId: event.visitorId,
|
|
3685
|
+
sessionId: event.sessionId
|
|
3686
|
+
});
|
|
3687
|
+
}
|
|
3688
|
+
if (eventIdentity && analyticsIdentityPromotionAllowed(event)) {
|
|
3689
|
+
await linkAnalyticsIdentityInTransaction(client, {
|
|
3690
|
+
siteId: pixel.site_id,
|
|
3691
|
+
visitorId: event.visitorId,
|
|
3692
|
+
sessionId: event.sessionId,
|
|
3693
|
+
email: eventIdentity.email,
|
|
3694
|
+
phone: eventIdentity.phone,
|
|
3695
|
+
customerId: eventIdentity.customerId,
|
|
3696
|
+
orderId: eventIdentity.orderId,
|
|
3697
|
+
clickIds: event.clickIds
|
|
3698
|
+
});
|
|
3699
|
+
}
|
|
3203
3700
|
} else {
|
|
3204
3701
|
rejected += 1;
|
|
3205
3702
|
reasonCodes.add("duplicate_event");
|
|
@@ -3222,7 +3719,7 @@ async function ingestAnalyticsEvents(input) {
|
|
|
3222
3719
|
id, request_id, site_id, pixel_id, hostname, accepted_count, rejected_count, reason_codes
|
|
3223
3720
|
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8::jsonb)`,
|
|
3224
3721
|
[
|
|
3225
|
-
|
|
3722
|
+
randomUUID2(),
|
|
3226
3723
|
requestId,
|
|
3227
3724
|
pixel.site_id,
|
|
3228
3725
|
pixel.id,
|
|
@@ -3254,7 +3751,7 @@ function normalizeGeographyCode(value, max) {
|
|
|
3254
3751
|
return normalized && /^[A-Z0-9-]+$/.test(normalized) ? normalized.slice(0, max) : null;
|
|
3255
3752
|
}
|
|
3256
3753
|
function pageFingerprint(scope, siteId, filters) {
|
|
3257
|
-
return
|
|
3754
|
+
return createHash2("sha256").update(JSON.stringify({ scope, siteId, filters })).digest("base64url").slice(0, 18);
|
|
3258
3755
|
}
|
|
3259
3756
|
function decodePageOffset(cursor, fingerprint) {
|
|
3260
3757
|
if (!cursor) return 0;
|
|
@@ -3313,7 +3810,7 @@ async function createAnalyticsConversion(input) {
|
|
|
3313
3810
|
ORDER BY e.confidence DESC LIMIT 1`,
|
|
3314
3811
|
[input.siteId, identityHmac("session_id", input.sessionId)]
|
|
3315
3812
|
) : { rows: [] };
|
|
3316
|
-
const proposedId =
|
|
3813
|
+
const proposedId = randomUUID2();
|
|
3317
3814
|
const inserted = await client.query(
|
|
3318
3815
|
`INSERT INTO analytics_conversions(
|
|
3319
3816
|
id, source_event_id, site_id, pixel_id, session_id, conversion_kind,
|
|
@@ -3353,7 +3850,7 @@ async function createAnalyticsConversion(input) {
|
|
|
3353
3850
|
) VALUES ($1,$2,$3,$4,$5)
|
|
3354
3851
|
ON CONFLICT(destination_id, conversion_id) DO NOTHING`,
|
|
3355
3852
|
[
|
|
3356
|
-
|
|
3853
|
+
randomUUID2(),
|
|
3357
3854
|
destination.id,
|
|
3358
3855
|
conversionId,
|
|
3359
3856
|
resolvedPerson.rows[0]?.person_id ?? null,
|
|
@@ -4117,7 +4614,7 @@ async function createAnalyticsCampaignLink(input) {
|
|
|
4117
4614
|
404
|
|
4118
4615
|
);
|
|
4119
4616
|
}
|
|
4120
|
-
const shortCode = (input.shortCode?.trim().toLowerCase().replace(/[^a-z0-9_-]/g, "-") ||
|
|
4617
|
+
const shortCode = (input.shortCode?.trim().toLowerCase().replace(/[^a-z0-9_-]/g, "-") || randomBytes2(5).toString("base64url").toLowerCase()).slice(0, 48);
|
|
4121
4618
|
try {
|
|
4122
4619
|
const result = await db.query(
|
|
4123
4620
|
`INSERT INTO analytics_campaign_links(
|
|
@@ -4126,7 +4623,7 @@ async function createAnalyticsCampaignLink(input) {
|
|
|
4126
4623
|
) VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14,$15)
|
|
4127
4624
|
RETURNING *, 0::int AS click_count`,
|
|
4128
4625
|
[
|
|
4129
|
-
|
|
4626
|
+
randomUUID2(),
|
|
4130
4627
|
input.siteId,
|
|
4131
4628
|
input.pixelId ?? null,
|
|
4132
4629
|
input.name.trim(),
|
|
@@ -4197,7 +4694,7 @@ async function resolveAnalyticsCampaignLink(shortCode, referrer) {
|
|
|
4197
4694
|
if (!row) return null;
|
|
4198
4695
|
await db.query(
|
|
4199
4696
|
`INSERT INTO analytics_campaign_clicks(id, link_id, referrer) VALUES ($1, $2, $3)`,
|
|
4200
|
-
[
|
|
4697
|
+
[randomUUID2(), row.id, normalizeAnalyticsUrl(referrer || void 0)]
|
|
4201
4698
|
);
|
|
4202
4699
|
return buildTaggedCampaignUrl(row);
|
|
4203
4700
|
}
|
|
@@ -4215,14 +4712,14 @@ async function createAnalyticsForm(input) {
|
|
|
4215
4712
|
404
|
|
4216
4713
|
);
|
|
4217
4714
|
const baseSlug = normalizeSlug(input.name);
|
|
4218
|
-
const slug = `${baseSlug}-${
|
|
4219
|
-
const publicId = `form_${
|
|
4715
|
+
const slug = `${baseSlug}-${randomBytes2(3).toString("hex")}`;
|
|
4716
|
+
const publicId = `form_${randomBytes2(18).toString("base64url")}`;
|
|
4220
4717
|
const result = await db.query(
|
|
4221
4718
|
`INSERT INTO analytics_forms(id, public_id, site_id, pixel_id, name, slug, fields, brand, submit_label, success_message, redirect_url, consent_text, status, created_by_user_id)
|
|
4222
4719
|
VALUES ($1,$2,$3,$4,$5,$6,$7::jsonb,$8::jsonb,$9,$10,$11,$12,$13,$14)
|
|
4223
4720
|
RETURNING *, 0::int AS submission_count`,
|
|
4224
4721
|
[
|
|
4225
|
-
|
|
4722
|
+
randomUUID2(),
|
|
4226
4723
|
publicId,
|
|
4227
4724
|
input.siteId,
|
|
4228
4725
|
input.pixelId,
|
|
@@ -4267,7 +4764,7 @@ async function getPublicAnalyticsForm(publicId) {
|
|
|
4267
4764
|
return result.rows[0] ?? null;
|
|
4268
4765
|
}
|
|
4269
4766
|
async function recordAnalyticsFormSubmission(input) {
|
|
4270
|
-
const id =
|
|
4767
|
+
const id = randomUUID2();
|
|
4271
4768
|
await getAnalyticsPool().query(
|
|
4272
4769
|
`INSERT INTO analytics_form_submissions(id, form_id, site_id, pixel_id, visitor_id, session_id, crm_person_ref, source, medium, campaign, crm_delivery_status, click_ids)
|
|
4273
4770
|
VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12::jsonb)`,
|
|
@@ -4311,16 +4808,128 @@ function sanitizeClickIds(value) {
|
|
|
4311
4808
|
return output;
|
|
4312
4809
|
}
|
|
4313
4810
|
function identityHmac(kind, value) {
|
|
4314
|
-
return
|
|
4811
|
+
return createHmac2("sha256", getSessionSecret()).update(`${kind}:${value.trim().toLowerCase()}`).digest("hex");
|
|
4812
|
+
}
|
|
4813
|
+
function newAnalyticsPersonPublicRef() {
|
|
4814
|
+
return `xrp_${randomBytes2(18).toString("base64url")}`;
|
|
4815
|
+
}
|
|
4816
|
+
async function ensurePrimaryAnalyticsIdentityNamespace(client, siteId, source) {
|
|
4817
|
+
let namespace = await client.query(
|
|
4818
|
+
"SELECT id FROM analytics_identity_namespaces WHERE site_id=$1 AND status='approved' ORDER BY version DESC LIMIT 1",
|
|
4819
|
+
[siteId]
|
|
4820
|
+
);
|
|
4821
|
+
if (!namespace.rows[0]) {
|
|
4822
|
+
await client.query(
|
|
4823
|
+
`INSERT INTO analytics_identity_namespaces(
|
|
4824
|
+
id,site_id,name,status,approved_hosts,approved_sources,version,approved_by_user_id,approved_at
|
|
4825
|
+
)
|
|
4826
|
+
SELECT $1,s.id,'X-Ray primary','approved',
|
|
4827
|
+
COALESCE((SELECT jsonb_agg(d.hostname ORDER BY d.hostname)
|
|
4828
|
+
FROM analytics_pixels p
|
|
4829
|
+
JOIN analytics_pixel_domains d ON d.pixel_id=p.id AND d.state='approved'
|
|
4830
|
+
WHERE p.site_id=s.id),'[]'::jsonb),
|
|
4831
|
+
$3::jsonb,1,s.owner_user_id,now()
|
|
4832
|
+
FROM analytics_sites s WHERE s.id=$2
|
|
4833
|
+
ON CONFLICT(site_id,name,version) DO NOTHING`,
|
|
4834
|
+
[randomUUID2(), siteId, JSON.stringify([source])]
|
|
4835
|
+
);
|
|
4836
|
+
namespace = await client.query(
|
|
4837
|
+
"SELECT id FROM analytics_identity_namespaces WHERE site_id=$1 AND status='approved' ORDER BY version DESC LIMIT 1",
|
|
4838
|
+
[siteId]
|
|
4839
|
+
);
|
|
4840
|
+
}
|
|
4841
|
+
const namespaceId = namespace.rows[0]?.id;
|
|
4842
|
+
if (!namespaceId)
|
|
4843
|
+
throw new AnalyticsRepositoryError(
|
|
4844
|
+
"identity_namespace_unavailable",
|
|
4845
|
+
"The Site identity graph could not be initialized.",
|
|
4846
|
+
409
|
|
4847
|
+
);
|
|
4848
|
+
await client.query(
|
|
4849
|
+
`UPDATE analytics_identity_namespaces
|
|
4850
|
+
SET approved_sources=(SELECT jsonb_agg(value ORDER BY value)
|
|
4851
|
+
FROM (SELECT DISTINCT value
|
|
4852
|
+
FROM jsonb_array_elements_text(approved_sources || $3::jsonb)) merged_sources),
|
|
4853
|
+
approved_hosts=(SELECT COALESCE(jsonb_agg(hostname ORDER BY hostname),'[]'::jsonb)
|
|
4854
|
+
FROM (SELECT DISTINCT hostname
|
|
4855
|
+
FROM (SELECT jsonb_array_elements_text(approved_hosts) AS hostname
|
|
4856
|
+
UNION ALL
|
|
4857
|
+
SELECT d.hostname
|
|
4858
|
+
FROM analytics_pixels p
|
|
4859
|
+
JOIN analytics_pixel_domains d ON d.pixel_id=p.id AND d.state='approved'
|
|
4860
|
+
WHERE p.site_id=$2) hosts) distinct_hosts),
|
|
4861
|
+
updated_at=now()
|
|
4862
|
+
WHERE id=$1 AND site_id=$2`,
|
|
4863
|
+
[namespaceId, siteId, JSON.stringify([source])]
|
|
4864
|
+
);
|
|
4865
|
+
return namespaceId;
|
|
4866
|
+
}
|
|
4867
|
+
async function backfillAnalyticsConfirmedHistory(client, input) {
|
|
4868
|
+
if (!input.visitorId && !input.sessionId)
|
|
4869
|
+
return { events: 0, sessions: 0, touches: 0, conversions: 0 };
|
|
4870
|
+
const visitorHmac = input.visitorId ? identityHmac("visitor_id", input.visitorId) : null;
|
|
4871
|
+
const sessionHmac = input.sessionId ? identityHmac("session_id", input.sessionId) : null;
|
|
4872
|
+
const events = await client.query(
|
|
4873
|
+
`UPDATE analytics_events SET person_id = $1 WHERE site_id = $2 AND person_id IS NULL
|
|
4874
|
+
AND (($3::text IS NOT NULL AND visitor_id = $3) OR ($4::text IS NOT NULL AND session_id = $4))`,
|
|
4875
|
+
[input.personId, input.siteId, input.visitorId ?? null, input.sessionId ?? null]
|
|
4876
|
+
);
|
|
4877
|
+
const conversions = await client.query(
|
|
4878
|
+
`UPDATE analytics_conversions SET person_id = $1 WHERE site_id = $2 AND person_id IS NULL
|
|
4879
|
+
AND $3::text IS NOT NULL AND session_id = $3`,
|
|
4880
|
+
[input.personId, input.siteId, input.sessionId ?? null]
|
|
4881
|
+
);
|
|
4882
|
+
const sessions = await client.query(
|
|
4883
|
+
`UPDATE analytics_sessions SET person_id = $1
|
|
4884
|
+
WHERE site_id = $2 AND person_id IS NULL
|
|
4885
|
+
AND (($3::text IS NOT NULL AND visitor_hmac = $3)
|
|
4886
|
+
OR ($4::text IS NOT NULL AND session_hmac = $4))`,
|
|
4887
|
+
[input.personId, input.siteId, visitorHmac, sessionHmac]
|
|
4888
|
+
);
|
|
4889
|
+
const touches = await client.query(
|
|
4890
|
+
`UPDATE analytics_attribution_touches SET person_id = $1
|
|
4891
|
+
WHERE site_id = $2 AND person_id IS NULL
|
|
4892
|
+
AND (($3::text IS NOT NULL AND visitor_hmac = $3)
|
|
4893
|
+
OR ($4::text IS NOT NULL AND session_id IN (
|
|
4894
|
+
SELECT id FROM analytics_sessions
|
|
4895
|
+
WHERE site_id = $2 AND session_hmac = $4
|
|
4896
|
+
)))`,
|
|
4897
|
+
[input.personId, input.siteId, visitorHmac, sessionHmac]
|
|
4898
|
+
);
|
|
4899
|
+
return {
|
|
4900
|
+
events: events.rowCount ?? 0,
|
|
4901
|
+
sessions: sessions.rowCount ?? 0,
|
|
4902
|
+
touches: touches.rowCount ?? 0,
|
|
4903
|
+
conversions: conversions.rowCount ?? 0
|
|
4904
|
+
};
|
|
4315
4905
|
}
|
|
4316
4906
|
async function linkAnalyticsIdentityInTransaction(client, input) {
|
|
4317
4907
|
const stable = input.customerId || input.orderId || input.email || input.phone;
|
|
4318
4908
|
if (!stable) return null;
|
|
4909
|
+
if (!await hasCurrentAnalyticsIdentityConsent(
|
|
4910
|
+
client,
|
|
4911
|
+
input.siteId,
|
|
4912
|
+
input.visitorId,
|
|
4913
|
+
input.sessionId
|
|
4914
|
+
)) return null;
|
|
4915
|
+
const namespaceId = await ensurePrimaryAnalyticsIdentityNamespace(client, input.siteId, "pixel");
|
|
4916
|
+
const tombstoneSignals = [
|
|
4917
|
+
...input.email ? [{ kind: "email", valueHmac: identityHmac("email", input.email) }] : [],
|
|
4918
|
+
...input.phone ? [{ kind: "phone", valueHmac: identityHmac("phone", input.phone) }] : [],
|
|
4919
|
+
...input.customerId ? [{ kind: "customer_id", valueHmac: identityHmac("customer_id", input.customerId) }] : [],
|
|
4920
|
+
...input.orderId ? [{ kind: "customer_id", valueHmac: identityHmac("customer_id", `order:${input.orderId}`) }] : []
|
|
4921
|
+
];
|
|
4922
|
+
await assertAnalyticsIdentitySignalsNotTombstoned(client, {
|
|
4923
|
+
siteId: input.siteId,
|
|
4924
|
+
signals: tombstoneSignals
|
|
4925
|
+
});
|
|
4319
4926
|
const crmPersonRef = `XRay::${identityHmac("person_ref", stable).slice(0, 24)}`;
|
|
4320
4927
|
const person = await client.query(
|
|
4321
|
-
`INSERT INTO analytics_people(id, site_id, crm_person_ref) VALUES ($1,$2,$3)
|
|
4322
|
-
ON CONFLICT(site_id, crm_person_ref) DO UPDATE SET last_seen_at = now()
|
|
4323
|
-
|
|
4928
|
+
`INSERT INTO analytics_people(id, site_id, namespace_id, crm_person_ref, public_ref) VALUES ($1,$2,$3,$4,$5)
|
|
4929
|
+
ON CONFLICT(site_id, crm_person_ref) DO UPDATE SET last_seen_at = now(),
|
|
4930
|
+
namespace_id=COALESCE(analytics_people.namespace_id,EXCLUDED.namespace_id),
|
|
4931
|
+
public_ref=COALESCE(analytics_people.public_ref,EXCLUDED.public_ref) RETURNING id`,
|
|
4932
|
+
[randomUUID2(), input.siteId, namespaceId, crmPersonRef, newAnalyticsPersonPublicRef()]
|
|
4324
4933
|
);
|
|
4325
4934
|
const personId = person.rows[0].id;
|
|
4326
4935
|
const signals = [];
|
|
@@ -4340,28 +4949,65 @@ async function linkAnalyticsIdentityInTransaction(client, input) {
|
|
|
4340
4949
|
signals.push({ kind: "click_id", value: `${platform}:${clickId}`, evidence: platform, confidence: 0.9 });
|
|
4341
4950
|
for (const signal of signals) {
|
|
4342
4951
|
const node = await client.query(
|
|
4343
|
-
`INSERT INTO analytics_identity_nodes(id, site_id, kind, value_hmac) VALUES ($1,$2,$3,$4)
|
|
4344
|
-
ON CONFLICT(site_id, kind, value_hmac) DO UPDATE SET last_seen_at = now()
|
|
4345
|
-
|
|
4952
|
+
`INSERT INTO analytics_identity_nodes(id, site_id, namespace_id, kind, value_hmac) VALUES ($1,$2,$3,$4,$5)
|
|
4953
|
+
ON CONFLICT(site_id, kind, value_hmac) DO UPDATE SET last_seen_at = now(),
|
|
4954
|
+
namespace_id=COALESCE(analytics_identity_nodes.namespace_id,EXCLUDED.namespace_id) RETURNING id`,
|
|
4955
|
+
[randomUUID2(), input.siteId, namespaceId, signal.kind, identityHmac(signal.kind, signal.value)]
|
|
4346
4956
|
);
|
|
4347
4957
|
await client.query(
|
|
4348
|
-
`INSERT INTO analytics_identity_edges(id, site_id, person_id, identity_node_id, evidence_kind, confidence)
|
|
4349
|
-
VALUES ($1,$2,$3,$4,$5,$6)
|
|
4958
|
+
`INSERT INTO analytics_identity_edges(id, site_id, namespace_id, person_id, identity_node_id, evidence_kind, confidence)
|
|
4959
|
+
VALUES ($1,$2,$3,$4,$5,$6,$7)
|
|
4350
4960
|
ON CONFLICT(person_id, identity_node_id, evidence_kind) DO UPDATE
|
|
4351
|
-
SET last_seen_at = now(), confidence = greatest(analytics_identity_edges.confidence, EXCLUDED.confidence)
|
|
4352
|
-
|
|
4961
|
+
SET last_seen_at = now(), confidence = greatest(analytics_identity_edges.confidence, EXCLUDED.confidence),
|
|
4962
|
+
namespace_id=COALESCE(analytics_identity_edges.namespace_id,EXCLUDED.namespace_id)`,
|
|
4963
|
+
[randomUUID2(), input.siteId, namespaceId, personId, node.rows[0].id, signal.evidence, signal.confidence]
|
|
4353
4964
|
);
|
|
4354
4965
|
}
|
|
4966
|
+
await backfillAnalyticsConfirmedHistory(client, {
|
|
4967
|
+
siteId: input.siteId,
|
|
4968
|
+
personId,
|
|
4969
|
+
visitorId: input.visitorId,
|
|
4970
|
+
sessionId: input.sessionId
|
|
4971
|
+
});
|
|
4972
|
+
if (input.customerId) {
|
|
4973
|
+
await writeAnalyticsConfirmedIdentityProfile(client, {
|
|
4974
|
+
siteId: input.siteId,
|
|
4975
|
+
personId,
|
|
4976
|
+
consentSubjectRefs: [input.visitorId ?? "", input.sessionId ?? ""],
|
|
4977
|
+
evidenceType: "authenticated_user",
|
|
4978
|
+
email: input.email,
|
|
4979
|
+
phone: input.phone
|
|
4980
|
+
});
|
|
4981
|
+
}
|
|
4355
4982
|
return personId;
|
|
4356
4983
|
}
|
|
4357
4984
|
async function linkAnalyticsFormIdentity(input) {
|
|
4358
4985
|
const client = await getAnalyticsPool().connect();
|
|
4359
4986
|
try {
|
|
4360
4987
|
await client.query("BEGIN");
|
|
4988
|
+
if (!await hasCurrentAnalyticsIdentityConsent(
|
|
4989
|
+
client,
|
|
4990
|
+
input.siteId,
|
|
4991
|
+
input.visitorId,
|
|
4992
|
+
input.sessionId
|
|
4993
|
+
)) {
|
|
4994
|
+
await client.query("COMMIT");
|
|
4995
|
+
return { personId: null, linkedSignals: 0 };
|
|
4996
|
+
}
|
|
4997
|
+
const namespaceId = await ensurePrimaryAnalyticsIdentityNamespace(client, input.siteId, "form");
|
|
4998
|
+
await assertAnalyticsIdentitySignalsNotTombstoned(client, {
|
|
4999
|
+
siteId: input.siteId,
|
|
5000
|
+
signals: [
|
|
5001
|
+
...input.email ? [{ kind: "email", valueHmac: identityHmac("email", input.email) }] : [],
|
|
5002
|
+
...input.phone ? [{ kind: "phone", valueHmac: identityHmac("phone", input.phone) }] : []
|
|
5003
|
+
]
|
|
5004
|
+
});
|
|
4361
5005
|
const person = await client.query(
|
|
4362
|
-
`INSERT INTO analytics_people(id, site_id, crm_person_ref) VALUES ($1,$2,$3)
|
|
4363
|
-
ON CONFLICT(site_id, crm_person_ref) DO UPDATE SET last_seen_at = now()
|
|
4364
|
-
|
|
5006
|
+
`INSERT INTO analytics_people(id, site_id, namespace_id, crm_person_ref, public_ref) VALUES ($1,$2,$3,$4,$5)
|
|
5007
|
+
ON CONFLICT(site_id, crm_person_ref) DO UPDATE SET last_seen_at = now(),
|
|
5008
|
+
namespace_id=COALESCE(analytics_people.namespace_id,EXCLUDED.namespace_id),
|
|
5009
|
+
public_ref=COALESCE(analytics_people.public_ref,EXCLUDED.public_ref) RETURNING id`,
|
|
5010
|
+
[randomUUID2(), input.siteId, namespaceId, input.crmPersonRef, newAnalyticsPersonPublicRef()]
|
|
4365
5011
|
);
|
|
4366
5012
|
const personId = person.rows[0].id;
|
|
4367
5013
|
const signals = [];
|
|
@@ -4405,22 +5051,27 @@ async function linkAnalyticsFormIdentity(input) {
|
|
|
4405
5051
|
}
|
|
4406
5052
|
for (const signal of signals) {
|
|
4407
5053
|
const node = await client.query(
|
|
4408
|
-
`INSERT INTO analytics_identity_nodes(id, site_id, kind, value_hmac) VALUES ($1,$2,$3,$4)
|
|
4409
|
-
ON CONFLICT(site_id, kind, value_hmac) DO UPDATE SET last_seen_at = now()
|
|
5054
|
+
`INSERT INTO analytics_identity_nodes(id, site_id, namespace_id, kind, value_hmac) VALUES ($1,$2,$3,$4,$5)
|
|
5055
|
+
ON CONFLICT(site_id, kind, value_hmac) DO UPDATE SET last_seen_at = now(),
|
|
5056
|
+
namespace_id=COALESCE(analytics_identity_nodes.namespace_id,EXCLUDED.namespace_id) RETURNING id`,
|
|
4410
5057
|
[
|
|
4411
|
-
|
|
5058
|
+
randomUUID2(),
|
|
4412
5059
|
input.siteId,
|
|
5060
|
+
namespaceId,
|
|
4413
5061
|
signal.kind,
|
|
4414
5062
|
identityHmac(signal.kind, signal.value)
|
|
4415
5063
|
]
|
|
4416
5064
|
);
|
|
4417
5065
|
await client.query(
|
|
4418
|
-
`INSERT INTO analytics_identity_edges(id, site_id, person_id, identity_node_id, evidence_kind, confidence)
|
|
4419
|
-
VALUES ($1,$2,$3,$4,$5,$6)
|
|
4420
|
-
ON CONFLICT(person_id, identity_node_id, evidence_kind) DO UPDATE SET last_seen_at = now(),
|
|
5066
|
+
`INSERT INTO analytics_identity_edges(id, site_id, namespace_id, person_id, identity_node_id, evidence_kind, confidence)
|
|
5067
|
+
VALUES ($1,$2,$3,$4,$5,$6,$7)
|
|
5068
|
+
ON CONFLICT(person_id, identity_node_id, evidence_kind) DO UPDATE SET last_seen_at = now(),
|
|
5069
|
+
confidence = greatest(analytics_identity_edges.confidence, EXCLUDED.confidence),
|
|
5070
|
+
namespace_id=COALESCE(analytics_identity_edges.namespace_id,EXCLUDED.namespace_id)`,
|
|
4421
5071
|
[
|
|
4422
|
-
|
|
5072
|
+
randomUUID2(),
|
|
4423
5073
|
input.siteId,
|
|
5074
|
+
namespaceId,
|
|
4424
5075
|
personId,
|
|
4425
5076
|
node.rows[0].id,
|
|
4426
5077
|
signal.evidence,
|
|
@@ -4434,42 +5085,80 @@ async function linkAnalyticsFormIdentity(input) {
|
|
|
4434
5085
|
[personId, input.submissionId, input.siteId]
|
|
4435
5086
|
);
|
|
4436
5087
|
if (input.visitorId || input.sessionId) {
|
|
4437
|
-
|
|
4438
|
-
|
|
4439
|
-
|
|
4440
|
-
|
|
4441
|
-
|
|
4442
|
-
|
|
4443
|
-
|
|
4444
|
-
|
|
4445
|
-
|
|
4446
|
-
|
|
4447
|
-
|
|
4448
|
-
|
|
4449
|
-
|
|
4450
|
-
|
|
4451
|
-
|
|
4452
|
-
|
|
4453
|
-
|
|
4454
|
-
|
|
4455
|
-
|
|
4456
|
-
|
|
4457
|
-
|
|
4458
|
-
|
|
5088
|
+
await backfillAnalyticsConfirmedHistory(client, {
|
|
5089
|
+
siteId: input.siteId,
|
|
5090
|
+
personId,
|
|
5091
|
+
visitorId: input.visitorId,
|
|
5092
|
+
sessionId: input.sessionId
|
|
5093
|
+
});
|
|
5094
|
+
}
|
|
5095
|
+
await writeAnalyticsConfirmedIdentityProfile(client, {
|
|
5096
|
+
siteId: input.siteId,
|
|
5097
|
+
personId,
|
|
5098
|
+
consentSubjectRefs: [input.visitorId ?? ""],
|
|
5099
|
+
evidenceType: input.evidenceType ?? "first_party_form",
|
|
5100
|
+
displayName: input.displayName,
|
|
5101
|
+
email: input.email,
|
|
5102
|
+
phone: input.phone
|
|
5103
|
+
});
|
|
5104
|
+
await client.query("COMMIT");
|
|
5105
|
+
return { personId, linkedSignals: signals.length };
|
|
5106
|
+
} catch (error) {
|
|
5107
|
+
await client.query("ROLLBACK");
|
|
5108
|
+
throw error;
|
|
5109
|
+
} finally {
|
|
5110
|
+
client.release();
|
|
5111
|
+
}
|
|
5112
|
+
}
|
|
5113
|
+
async function enrichAnalyticsExistingCrmIdentity(input) {
|
|
5114
|
+
const client = await getAnalyticsPool().connect();
|
|
5115
|
+
try {
|
|
5116
|
+
await client.query("BEGIN");
|
|
5117
|
+
const existing = await client.query(
|
|
5118
|
+
`SELECT p.id,p.public_ref,p.namespace_id FROM analytics_people p
|
|
5119
|
+
JOIN analytics_identity_namespaces n ON n.id=p.namespace_id AND n.site_id=p.site_id AND n.status='approved'
|
|
5120
|
+
WHERE p.site_id=$1 AND p.crm_person_ref=$2 LIMIT 1 FOR UPDATE OF p`,
|
|
5121
|
+
[input.siteId, input.crmPersonRef]
|
|
5122
|
+
);
|
|
5123
|
+
const person = existing.rows[0];
|
|
5124
|
+
if (!person || !await hasCurrentPersonIdentityConsent(client, input.siteId, person.public_ref)) {
|
|
5125
|
+
await client.query("COMMIT");
|
|
5126
|
+
return { personId: null, linkedSignals: 0 };
|
|
5127
|
+
}
|
|
5128
|
+
const signals = [
|
|
5129
|
+
...input.email ? [{ kind: "email", value: input.email, confidence: 1 }] : [],
|
|
5130
|
+
...input.phone ? [{ kind: "phone", value: input.phone, confidence: 0.99 }] : []
|
|
5131
|
+
];
|
|
5132
|
+
await assertAnalyticsIdentitySignalsNotTombstoned(client, {
|
|
5133
|
+
siteId: input.siteId,
|
|
5134
|
+
signals: signals.map((signal) => ({ kind: signal.kind, valueHmac: identityHmac(signal.kind, signal.value) }))
|
|
5135
|
+
});
|
|
5136
|
+
for (const signal of signals) {
|
|
5137
|
+
const node = await client.query(
|
|
5138
|
+
`INSERT INTO analytics_identity_nodes(id,site_id,namespace_id,kind,value_hmac) VALUES($1,$2,$3,$4,$5)
|
|
5139
|
+
ON CONFLICT(site_id,kind,value_hmac) DO UPDATE SET last_seen_at=now(),
|
|
5140
|
+
namespace_id=COALESCE(analytics_identity_nodes.namespace_id,EXCLUDED.namespace_id) RETURNING id`,
|
|
5141
|
+
[randomUUID2(), input.siteId, person.namespace_id, signal.kind, identityHmac(signal.kind, signal.value)]
|
|
4459
5142
|
);
|
|
4460
5143
|
await client.query(
|
|
4461
|
-
`
|
|
4462
|
-
|
|
4463
|
-
|
|
4464
|
-
|
|
4465
|
-
|
|
4466
|
-
WHERE site_id = $2 AND session_hmac = $4
|
|
4467
|
-
)))`,
|
|
4468
|
-
[personId, input.siteId, visitorHmac, sessionHmac]
|
|
5144
|
+
`INSERT INTO analytics_identity_edges(id,site_id,namespace_id,person_id,identity_node_id,evidence_kind,confidence)
|
|
5145
|
+
VALUES($1,$2,$3,$4,$5,'crm_contact',$6)
|
|
5146
|
+
ON CONFLICT(person_id,identity_node_id,evidence_kind) DO UPDATE SET last_seen_at=now(),
|
|
5147
|
+
confidence=greatest(analytics_identity_edges.confidence,EXCLUDED.confidence)`,
|
|
5148
|
+
[randomUUID2(), input.siteId, person.namespace_id, person.id, node.rows[0].id, signal.confidence]
|
|
4469
5149
|
);
|
|
4470
5150
|
}
|
|
5151
|
+
await writeAnalyticsConfirmedIdentityProfile(client, {
|
|
5152
|
+
siteId: input.siteId,
|
|
5153
|
+
personId: person.id,
|
|
5154
|
+
consentSubjectRefs: [person.public_ref],
|
|
5155
|
+
evidenceType: "crm_contact",
|
|
5156
|
+
displayName: input.displayName,
|
|
5157
|
+
email: input.email,
|
|
5158
|
+
phone: input.phone
|
|
5159
|
+
});
|
|
4471
5160
|
await client.query("COMMIT");
|
|
4472
|
-
return { personId, linkedSignals: signals.length };
|
|
5161
|
+
return { personId: person.id, linkedSignals: signals.length };
|
|
4473
5162
|
} catch (error) {
|
|
4474
5163
|
await client.query("ROLLBACK");
|
|
4475
5164
|
throw error;
|
|
@@ -4536,7 +5225,7 @@ async function createAnalyticsCrmImport(input) {
|
|
|
4536
5225
|
const db = getAnalyticsPool();
|
|
4537
5226
|
await requireEditor(db, input.siteId, input.userId);
|
|
4538
5227
|
const client = await db.connect();
|
|
4539
|
-
const id =
|
|
5228
|
+
const id = randomUUID2();
|
|
4540
5229
|
try {
|
|
4541
5230
|
await client.query("BEGIN");
|
|
4542
5231
|
await client.query(
|
|
@@ -4556,7 +5245,7 @@ async function createAnalyticsCrmImport(input) {
|
|
|
4556
5245
|
await client.query(
|
|
4557
5246
|
`INSERT INTO analytics_crm_import_rows(id, import_id, crm_person_ref, payload_ciphertext)
|
|
4558
5247
|
VALUES ($1,$2,$3,$4) ON CONFLICT(import_id, crm_person_ref) DO NOTHING`,
|
|
4559
|
-
[
|
|
5248
|
+
[randomUUID2(), id, row.crmPersonRef, row.payloadCiphertext]
|
|
4560
5249
|
);
|
|
4561
5250
|
}
|
|
4562
5251
|
await client.query("COMMIT");
|
|
@@ -4679,7 +5368,7 @@ async function createAnalyticsActivationDestination(input) {
|
|
|
4679
5368
|
`INSERT INTO analytics_activation_destinations(id, site_id, platform, name, connection_ref, external_dataset_id, event_mapping, provider_config, created_by_user_id)
|
|
4680
5369
|
VALUES ($1,$2,$3,$4,$5,$6,$7::jsonb,$8::jsonb,$9) RETURNING *`,
|
|
4681
5370
|
[
|
|
4682
|
-
|
|
5371
|
+
randomUUID2(),
|
|
4683
5372
|
input.siteId,
|
|
4684
5373
|
input.platform,
|
|
4685
5374
|
input.name.trim(),
|
|
@@ -4826,8 +5515,8 @@ async function testAnalyticsActivationDestination(input) {
|
|
|
4826
5515
|
400
|
|
4827
5516
|
);
|
|
4828
5517
|
}
|
|
4829
|
-
const testId =
|
|
4830
|
-
const syntheticHash =
|
|
5518
|
+
const testId = randomUUID2();
|
|
5519
|
+
const syntheticHash = createHash2("sha256").update(`xray-provider-test:${testId}`).digest("hex");
|
|
4831
5520
|
const providerConfig = destination.provider_config ?? {};
|
|
4832
5521
|
const payload = {
|
|
4833
5522
|
eventId: `xray-test:${testId}`,
|
|
@@ -5064,7 +5753,7 @@ async function queueAnalyticsActivation(input) {
|
|
|
5064
5753
|
`INSERT INTO analytics_activation_jobs(id, destination_id, conversion_id, person_id, payload_ciphertext)
|
|
5065
5754
|
VALUES ($1,$2,$3,$4,$5) ON CONFLICT(destination_id, conversion_id) DO NOTHING`,
|
|
5066
5755
|
[
|
|
5067
|
-
|
|
5756
|
+
randomUUID2(),
|
|
5068
5757
|
destination.id,
|
|
5069
5758
|
input.conversionId,
|
|
5070
5759
|
input.personId ?? null,
|
|
@@ -5076,7 +5765,7 @@ async function queueAnalyticsActivation(input) {
|
|
|
5076
5765
|
return queued;
|
|
5077
5766
|
}
|
|
5078
5767
|
async function queueAnalyticsFormDelivery(input) {
|
|
5079
|
-
const id =
|
|
5768
|
+
const id = randomUUID2();
|
|
5080
5769
|
await getAnalyticsPool().query(
|
|
5081
5770
|
`INSERT INTO analytics_form_delivery_jobs(id, submission_id, owner_user_id, payload_ciphertext, last_error_code)
|
|
5082
5771
|
VALUES ($1,$2,$3,$4,$5)`,
|
|
@@ -5233,7 +5922,7 @@ async function analyticsHealth(siteId, userId) {
|
|
|
5233
5922
|
}
|
|
5234
5923
|
async function refreshAnalyticsDailyRollups(input) {
|
|
5235
5924
|
const db = getAnalyticsPool();
|
|
5236
|
-
const runId =
|
|
5925
|
+
const runId = randomUUID2();
|
|
5237
5926
|
await db.query(
|
|
5238
5927
|
`INSERT INTO analytics_rollup_runs(id, window_start, window_end, status) VALUES ($1, $2, $3, 'running')`,
|
|
5239
5928
|
[runId, input.start, input.end]
|
|
@@ -5430,7 +6119,7 @@ async function createAnalyticsExport(input) {
|
|
|
5430
6119
|
`Generated ${(/* @__PURE__ */ new Date()).toISOString()} from the governed ${input.report} report contract.`
|
|
5431
6120
|
].join("\n");
|
|
5432
6121
|
}
|
|
5433
|
-
const id =
|
|
6122
|
+
const id = randomUUID2();
|
|
5434
6123
|
const inserted = await getAnalyticsPool().query(
|
|
5435
6124
|
`INSERT INTO analytics_exports(id, site_id, requested_by_user_id, idempotency_key, report, format, filters, content)
|
|
5436
6125
|
VALUES ($1, $2, $3, $4, $5, $6, $7::jsonb, $8)
|
|
@@ -5465,6 +6154,13 @@ async function createAnalyticsExport(input) {
|
|
|
5465
6154
|
}
|
|
5466
6155
|
|
|
5467
6156
|
export {
|
|
6157
|
+
encryptAnalyticsRestricted,
|
|
6158
|
+
decryptAnalyticsRestricted,
|
|
6159
|
+
analyticsIdentityHmac,
|
|
6160
|
+
signAnalyticsWebhook,
|
|
6161
|
+
verifyAnalyticsWebhookSignature,
|
|
6162
|
+
AnalyticsIdentityProfileError,
|
|
6163
|
+
resolveAnalyticsMaskedIdentityProfile,
|
|
5468
6164
|
AnalyticsProviderRegistryError,
|
|
5469
6165
|
getAnalyticsProviderDefinition,
|
|
5470
6166
|
assertConnectionSupportsProvider,
|
|
@@ -5492,6 +6188,10 @@ export {
|
|
|
5492
6188
|
listAnalyticsHostGroups,
|
|
5493
6189
|
prepareAnalyticsLinkerIssue,
|
|
5494
6190
|
redeemAnalyticsLinkerRecord,
|
|
6191
|
+
analyticsIdentityResolutionAllowed,
|
|
6192
|
+
analyticsIdentityPromotionAllowed,
|
|
6193
|
+
projectAnalyticsPixelEventConsent,
|
|
6194
|
+
resolveAnalyticsConfirmedActivationIdentity,
|
|
5495
6195
|
MAX_ENGAGED_MS,
|
|
5496
6196
|
ENGAGED_SESSION_MS,
|
|
5497
6197
|
normalizeAnalyticsPath,
|
|
@@ -5520,7 +6220,10 @@ export {
|
|
|
5520
6220
|
recordAnalyticsFormSubmission,
|
|
5521
6221
|
sanitizeClickIds,
|
|
5522
6222
|
identityHmac,
|
|
6223
|
+
backfillAnalyticsConfirmedHistory,
|
|
6224
|
+
linkAnalyticsIdentityInTransaction,
|
|
5523
6225
|
linkAnalyticsFormIdentity,
|
|
6226
|
+
enrichAnalyticsExistingCrmIdentity,
|
|
5524
6227
|
listAnalyticsPeople,
|
|
5525
6228
|
getAnalyticsPersonJourney,
|
|
5526
6229
|
createAnalyticsCrmImport,
|