mcp-scraper 0.85.0 → 0.86.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 +23 -1
- package/README.md +2 -2
- package/dist/{analytics-repository-4VZQ4TKW.js → analytics-repository-GGJJCVVP.js} +9 -1
- package/dist/bin/api-server.js +3 -3
- package/dist/bin/mcp-scraper-cli.js +1 -1
- package/dist/bin/mcp-scraper-core.js +5 -5
- package/dist/bin/mcp-scraper-install.js +2 -2
- package/dist/bin/mcp-stdio-server.js +5 -5
- package/dist/{chunk-RHAOKXQK.js → chunk-36EAKRR6.js} +1 -1
- package/dist/{chunk-WN7N4ESQ.js → chunk-7523VOQT.js} +1 -1
- package/dist/{chunk-W4IY6CB4.js → chunk-FBB6DEUV.js} +5688 -4
- package/dist/{chunk-C7OKENCR.js → chunk-KQWVJOVR.js} +1 -1
- package/dist/{chunk-X5HRAAQM.js → chunk-NKRO2SUO.js} +3 -3
- package/dist/{chunk-2ZDYUTOZ.js → chunk-QKWNEMGH.js} +3 -3
- package/dist/{chunk-W3SGF7GX.js → chunk-WFGAR4BZ.js} +1 -1
- package/dist/{chunk-TLRJV4SL.js → chunk-X623GTBV.js} +427 -155
- package/dist/{extract-bundle-NSMGHEG6.js → extract-bundle-FALNLKYY.js} +2 -2
- package/dist/{gmail-service-BTRAHGJS.js → gmail-service-BUSU4BVX.js} +2 -2
- package/dist/{server-EOPETL7Q.js → server-OZBTZIDZ.js} +1320 -645
- package/dist/{site-extract-repository-DWU52BRQ.js → site-extract-repository-S5FYNFDY.js} +2 -2
- package/dist/{worker-5A375BGN.js → worker-OQSFSLNG.js} +1 -1
- package/package.json +15 -4
|
@@ -1688,6 +1688,20 @@ async function migrateAnalytics() {
|
|
|
1688
1688
|
await client.query(
|
|
1689
1689
|
`ALTER TABLE analytics_form_submissions ADD COLUMN IF NOT EXISTS crm_delivery_status text NOT NULL DEFAULT 'pending'`
|
|
1690
1690
|
);
|
|
1691
|
+
await client.query(
|
|
1692
|
+
`ALTER TABLE analytics_form_submissions ADD COLUMN IF NOT EXISTS crm_entity_id uuid`
|
|
1693
|
+
);
|
|
1694
|
+
await client.query(
|
|
1695
|
+
`ALTER TABLE analytics_form_submissions ADD COLUMN IF NOT EXISTS bridge_run_id uuid`
|
|
1696
|
+
);
|
|
1697
|
+
await client.query(
|
|
1698
|
+
`ALTER TABLE analytics_form_submissions ADD COLUMN IF NOT EXISTS idempotency_key text`
|
|
1699
|
+
);
|
|
1700
|
+
await client.query(
|
|
1701
|
+
`CREATE UNIQUE INDEX IF NOT EXISTS analytics_form_submissions_idempotency
|
|
1702
|
+
ON analytics_form_submissions(form_id, idempotency_key)
|
|
1703
|
+
WHERE idempotency_key IS NOT NULL`
|
|
1704
|
+
);
|
|
1691
1705
|
await client.query(`
|
|
1692
1706
|
CREATE TABLE IF NOT EXISTS analytics_form_delivery_jobs (
|
|
1693
1707
|
id uuid PRIMARY KEY,
|
|
@@ -1703,6 +1717,29 @@ async function migrateAnalytics() {
|
|
|
1703
1717
|
delivered_at timestamptz
|
|
1704
1718
|
)
|
|
1705
1719
|
`);
|
|
1720
|
+
await client.query(
|
|
1721
|
+
`ALTER TABLE analytics_form_delivery_jobs ADD COLUMN IF NOT EXISTS bridge_run_id uuid`
|
|
1722
|
+
);
|
|
1723
|
+
await client.query(
|
|
1724
|
+
`ALTER TABLE analytics_form_delivery_jobs ADD COLUMN IF NOT EXISTS crm_entity_id uuid`
|
|
1725
|
+
);
|
|
1726
|
+
await client.query(
|
|
1727
|
+
`ALTER TABLE analytics_form_delivery_jobs ADD COLUMN IF NOT EXISTS idempotency_key text`
|
|
1728
|
+
);
|
|
1729
|
+
await client.query(`
|
|
1730
|
+
CREATE TABLE IF NOT EXISTS analytics_form_bridge_receipts (
|
|
1731
|
+
id uuid PRIMARY KEY,
|
|
1732
|
+
bridge_run_id uuid NOT NULL,
|
|
1733
|
+
submission_id uuid NOT NULL REFERENCES analytics_form_submissions(id) ON DELETE CASCADE,
|
|
1734
|
+
system text NOT NULL CHECK (system IN ('analytics','crm')),
|
|
1735
|
+
status text NOT NULL CHECK (status IN ('pending','delivered','failed','expired')),
|
|
1736
|
+
opaque_entity_id uuid,
|
|
1737
|
+
safe_error_code text,
|
|
1738
|
+
occurred_at timestamptz NOT NULL DEFAULT now(),
|
|
1739
|
+
updated_at timestamptz NOT NULL DEFAULT now(),
|
|
1740
|
+
UNIQUE(bridge_run_id, system)
|
|
1741
|
+
)
|
|
1742
|
+
`);
|
|
1706
1743
|
await client.query(`
|
|
1707
1744
|
CREATE TABLE IF NOT EXISTS analytics_people (
|
|
1708
1745
|
id uuid PRIMARY KEY,
|
|
@@ -1713,6 +1750,14 @@ async function migrateAnalytics() {
|
|
|
1713
1750
|
UNIQUE(site_id, crm_person_ref)
|
|
1714
1751
|
)
|
|
1715
1752
|
`);
|
|
1753
|
+
await client.query(
|
|
1754
|
+
`ALTER TABLE analytics_people ADD COLUMN IF NOT EXISTS crm_entity_id uuid`
|
|
1755
|
+
);
|
|
1756
|
+
await client.query(
|
|
1757
|
+
`CREATE UNIQUE INDEX IF NOT EXISTS analytics_people_crm_entity
|
|
1758
|
+
ON analytics_people(site_id, crm_entity_id)
|
|
1759
|
+
WHERE crm_entity_id IS NOT NULL`
|
|
1760
|
+
);
|
|
1716
1761
|
await client.query(`
|
|
1717
1762
|
CREATE TABLE IF NOT EXISTS analytics_identity_nodes (
|
|
1718
1763
|
id uuid PRIMARY KEY,
|
|
@@ -1738,6 +1783,72 @@ async function migrateAnalytics() {
|
|
|
1738
1783
|
UNIQUE(person_id, identity_node_id, evidence_kind)
|
|
1739
1784
|
)
|
|
1740
1785
|
`);
|
|
1786
|
+
await client.query(`
|
|
1787
|
+
CREATE TABLE IF NOT EXISTS analytics_identity_assignments (
|
|
1788
|
+
identity_node_id uuid PRIMARY KEY REFERENCES analytics_identity_nodes(id) ON DELETE CASCADE,
|
|
1789
|
+
site_id uuid NOT NULL REFERENCES analytics_sites(id) ON DELETE CASCADE,
|
|
1790
|
+
person_id uuid NOT NULL REFERENCES analytics_people(id) ON DELETE CASCADE,
|
|
1791
|
+
crm_entity_id uuid,
|
|
1792
|
+
evidence_kind text NOT NULL,
|
|
1793
|
+
assigned_at timestamptz NOT NULL DEFAULT now(),
|
|
1794
|
+
updated_at timestamptz NOT NULL DEFAULT now(),
|
|
1795
|
+
CHECK (crm_entity_id IS NULL OR crm_entity_id::text <> '')
|
|
1796
|
+
)
|
|
1797
|
+
`);
|
|
1798
|
+
await client.query(`
|
|
1799
|
+
CREATE TABLE IF NOT EXISTS analytics_identity_assignment_conflicts (
|
|
1800
|
+
id uuid PRIMARY KEY,
|
|
1801
|
+
site_id uuid NOT NULL REFERENCES analytics_sites(id) ON DELETE CASCADE,
|
|
1802
|
+
identity_node_id uuid REFERENCES analytics_identity_nodes(id) ON DELETE CASCADE,
|
|
1803
|
+
active_person_id uuid REFERENCES analytics_people(id) ON DELETE CASCADE,
|
|
1804
|
+
attempted_person_id uuid REFERENCES analytics_people(id) ON DELETE CASCADE,
|
|
1805
|
+
active_crm_entity_id uuid,
|
|
1806
|
+
attempted_crm_entity_id uuid,
|
|
1807
|
+
evidence_kind text NOT NULL,
|
|
1808
|
+
status text NOT NULL DEFAULT 'pending' CHECK (status IN ('pending','resolved','dismissed')),
|
|
1809
|
+
observed_at timestamptz NOT NULL DEFAULT now(),
|
|
1810
|
+
resolved_at timestamptz,
|
|
1811
|
+
CONSTRAINT analytics_identity_assignment_conflicts_pending_identity
|
|
1812
|
+
CHECK (status <> 'pending' OR (
|
|
1813
|
+
identity_node_id IS NOT NULL AND active_person_id IS NOT NULL
|
|
1814
|
+
AND attempted_person_id IS NOT NULL AND active_person_id <> attempted_person_id
|
|
1815
|
+
))
|
|
1816
|
+
)
|
|
1817
|
+
`);
|
|
1818
|
+
await client.query(
|
|
1819
|
+
`ALTER TABLE analytics_identity_assignment_conflicts ALTER COLUMN identity_node_id DROP NOT NULL`
|
|
1820
|
+
);
|
|
1821
|
+
await client.query(
|
|
1822
|
+
`ALTER TABLE analytics_identity_assignment_conflicts ALTER COLUMN active_person_id DROP NOT NULL`
|
|
1823
|
+
);
|
|
1824
|
+
await client.query(
|
|
1825
|
+
`ALTER TABLE analytics_identity_assignment_conflicts ALTER COLUMN attempted_person_id DROP NOT NULL`
|
|
1826
|
+
);
|
|
1827
|
+
await client.query(
|
|
1828
|
+
`ALTER TABLE analytics_identity_assignment_conflicts
|
|
1829
|
+
DROP CONSTRAINT IF EXISTS analytics_identity_assignment_conflicts_check`
|
|
1830
|
+
);
|
|
1831
|
+
await client.query(`
|
|
1832
|
+
DO $$ BEGIN
|
|
1833
|
+
IF NOT EXISTS (
|
|
1834
|
+
SELECT 1 FROM pg_constraint
|
|
1835
|
+
WHERE conrelid='analytics_identity_assignment_conflicts'::regclass
|
|
1836
|
+
AND conname='analytics_identity_assignment_conflicts_pending_identity'
|
|
1837
|
+
) THEN
|
|
1838
|
+
ALTER TABLE analytics_identity_assignment_conflicts
|
|
1839
|
+
ADD CONSTRAINT analytics_identity_assignment_conflicts_pending_identity
|
|
1840
|
+
CHECK (status <> 'pending' OR (
|
|
1841
|
+
identity_node_id IS NOT NULL AND active_person_id IS NOT NULL
|
|
1842
|
+
AND attempted_person_id IS NOT NULL AND active_person_id <> attempted_person_id
|
|
1843
|
+
));
|
|
1844
|
+
END IF;
|
|
1845
|
+
END $$
|
|
1846
|
+
`);
|
|
1847
|
+
await client.query(
|
|
1848
|
+
`CREATE UNIQUE INDEX IF NOT EXISTS analytics_identity_assignment_conflicts_pending
|
|
1849
|
+
ON analytics_identity_assignment_conflicts(identity_node_id, active_person_id, attempted_person_id)
|
|
1850
|
+
WHERE status='pending'`
|
|
1851
|
+
);
|
|
1741
1852
|
await client.query(`
|
|
1742
1853
|
CREATE TABLE IF NOT EXISTS analytics_linker_tokens (
|
|
1743
1854
|
id uuid PRIMARY KEY,
|
|
@@ -4131,9 +4242,8 @@ async function prepareAnalyticsLinkerIssue(input) {
|
|
|
4131
4242
|
[randomUUID2(), siteId, identityHmac("visitor_id", input.visitorId)]
|
|
4132
4243
|
);
|
|
4133
4244
|
const existingPerson = await client.query(
|
|
4134
|
-
`SELECT person_id FROM
|
|
4135
|
-
WHERE site_id=$1 AND identity_node_id=$2
|
|
4136
|
-
ORDER BY confidence DESC, first_seen_at ASC LIMIT 1`,
|
|
4245
|
+
`SELECT person_id FROM analytics_identity_assignments
|
|
4246
|
+
WHERE site_id=$1 AND identity_node_id=$2`,
|
|
4137
4247
|
[siteId, node.rows[0].id]
|
|
4138
4248
|
);
|
|
4139
4249
|
let personId = existingPerson.rows[0]?.person_id;
|
|
@@ -4151,6 +4261,12 @@ async function prepareAnalyticsLinkerIssue(input) {
|
|
|
4151
4261
|
ON CONFLICT(person_id, identity_node_id, evidence_kind) DO UPDATE SET last_seen_at=now()`,
|
|
4152
4262
|
[randomUUID2(), siteId, personId, node.rows[0].id]
|
|
4153
4263
|
);
|
|
4264
|
+
await assignAnalyticsIdentityNode(client, {
|
|
4265
|
+
siteId,
|
|
4266
|
+
personId,
|
|
4267
|
+
identityNodeId: node.rows[0].id,
|
|
4268
|
+
evidenceKind: "first_party_linker"
|
|
4269
|
+
});
|
|
4154
4270
|
}
|
|
4155
4271
|
const tokenId = randomUUID2();
|
|
4156
4272
|
await client.query(
|
|
@@ -4195,9 +4311,8 @@ async function redeemAnalyticsLinkerRecord(input) {
|
|
|
4195
4311
|
409
|
|
4196
4312
|
);
|
|
4197
4313
|
const sourceEdge = await client.query(
|
|
4198
|
-
`SELECT person_id FROM
|
|
4199
|
-
WHERE site_id=$1 AND identity_node_id=$2
|
|
4200
|
-
ORDER BY confidence DESC, first_seen_at ASC LIMIT 1`,
|
|
4314
|
+
`SELECT person_id FROM analytics_identity_assignments
|
|
4315
|
+
WHERE site_id=$1 AND identity_node_id=$2`,
|
|
4201
4316
|
[input.siteId, input.identityNodeId]
|
|
4202
4317
|
);
|
|
4203
4318
|
const personId = sourceEdge.rows[0]?.person_id;
|
|
@@ -4216,6 +4331,18 @@ async function redeemAnalyticsLinkerRecord(input) {
|
|
|
4216
4331
|
ON CONFLICT(person_id, identity_node_id, evidence_kind) DO UPDATE SET last_seen_at=now()`,
|
|
4217
4332
|
[randomUUID2(), input.siteId, personId, targetNode.rows[0].id]
|
|
4218
4333
|
);
|
|
4334
|
+
const targetAssignment = await assignAnalyticsIdentityNode(client, {
|
|
4335
|
+
siteId: input.siteId,
|
|
4336
|
+
personId,
|
|
4337
|
+
identityNodeId: targetNode.rows[0].id,
|
|
4338
|
+
evidenceKind: "cross_domain_linker"
|
|
4339
|
+
});
|
|
4340
|
+
if (targetAssignment === "conflict")
|
|
4341
|
+
throw new AnalyticsRepositoryError(
|
|
4342
|
+
"identity_assignment_conflict",
|
|
4343
|
+
"The destination identity is already assigned and requires review.",
|
|
4344
|
+
409
|
|
4345
|
+
);
|
|
4219
4346
|
await client.query(
|
|
4220
4347
|
"UPDATE analytics_events SET person_id=$1 WHERE site_id=$2 AND visitor_id=$3 AND person_id IS NULL",
|
|
4221
4348
|
[personId, input.siteId, input.visitorId]
|
|
@@ -6256,6 +6383,12 @@ async function backfillAnalyticsJourneyLinkageHmacs(client) {
|
|
|
6256
6383
|
function newAnalyticsPersonPublicRef() {
|
|
6257
6384
|
return `xrp_${randomBytes2(18).toString("base64url")}`;
|
|
6258
6385
|
}
|
|
6386
|
+
function deterministicAnalyticsCrmEntityId(siteId, stableIdentity) {
|
|
6387
|
+
const hex = identityHmac("crm_entity_id", `${siteId}:${stableIdentity}`).slice(0, 32).split("");
|
|
6388
|
+
hex[12] = "5";
|
|
6389
|
+
hex[16] = ["8", "9", "a", "b"][Number.parseInt(hex[16], 16) % 4];
|
|
6390
|
+
return `${hex.slice(0, 8).join("")}-${hex.slice(8, 12).join("")}-${hex.slice(12, 16).join("")}-${hex.slice(16, 20).join("")}-${hex.slice(20).join("")}`;
|
|
6391
|
+
}
|
|
6259
6392
|
async function ensurePrimaryAnalyticsIdentityNamespace(client, siteId, source) {
|
|
6260
6393
|
let namespace = await client.query(
|
|
6261
6394
|
"SELECT id FROM analytics_identity_namespaces WHERE site_id=$1 AND status='approved' ORDER BY version DESC LIMIT 1",
|
|
@@ -6347,142 +6480,107 @@ async function backfillAnalyticsConfirmedHistory(client, input) {
|
|
|
6347
6480
|
};
|
|
6348
6481
|
}
|
|
6349
6482
|
async function resolveAndMergeConfirmedAnalyticsPerson(client, input) {
|
|
6350
|
-
const
|
|
6351
|
-
`SELECT
|
|
6352
|
-
|
|
6353
|
-
|
|
6354
|
-
|
|
6355
|
-
|
|
6356
|
-
|
|
6357
|
-
|
|
6358
|
-
|
|
6359
|
-
|
|
6360
|
-
|
|
6361
|
-
|
|
6362
|
-
|
|
6363
|
-
|
|
6364
|
-
|
|
6365
|
-
|
|
6366
|
-
|
|
6367
|
-
|
|
6368
|
-
|
|
6369
|
-
|
|
6370
|
-
input.
|
|
6371
|
-
|
|
6372
|
-
]
|
|
6373
|
-
|
|
6374
|
-
|
|
6375
|
-
|
|
6483
|
+
const explicit = await client.query(
|
|
6484
|
+
`SELECT id FROM analytics_people
|
|
6485
|
+
WHERE site_id=$1 AND (namespace_id=$2 OR namespace_id IS NULL)
|
|
6486
|
+
AND (($3::uuid IS NOT NULL AND crm_entity_id=$3::uuid)
|
|
6487
|
+
OR ($3::uuid IS NULL AND $4::text IS NOT NULL AND crm_person_ref=$4))
|
|
6488
|
+
ORDER BY first_seen_at,id LIMIT 1 FOR UPDATE`,
|
|
6489
|
+
[input.siteId, input.namespaceId, input.crmEntityId ?? null, input.crmPersonRef ?? null]
|
|
6490
|
+
);
|
|
6491
|
+
let personId = explicit.rows[0]?.id;
|
|
6492
|
+
if (!personId && !input.crmEntityId && !input.crmPersonRef && input.signals.length) {
|
|
6493
|
+
const assigned = await client.query(
|
|
6494
|
+
`SELECT assignment.person_id
|
|
6495
|
+
FROM analytics_identity_nodes node
|
|
6496
|
+
JOIN analytics_identity_assignments assignment
|
|
6497
|
+
ON assignment.identity_node_id=node.id AND assignment.site_id=node.site_id
|
|
6498
|
+
WHERE node.site_id=$1 AND EXISTS (
|
|
6499
|
+
SELECT 1 FROM jsonb_to_recordset($2::jsonb) signal(kind text,value_hmac text)
|
|
6500
|
+
WHERE signal.kind=node.kind AND signal.value_hmac=node.value_hmac
|
|
6501
|
+
)
|
|
6502
|
+
ORDER BY assignment.assigned_at,assignment.person_id LIMIT 1`,
|
|
6503
|
+
[input.siteId, JSON.stringify(input.signals.map((signal) => ({ kind: signal.kind, value_hmac: signal.valueHmac })))]
|
|
6504
|
+
);
|
|
6505
|
+
personId = assigned.rows[0]?.person_id;
|
|
6506
|
+
}
|
|
6507
|
+
if (!personId) {
|
|
6508
|
+
const entityId = input.crmEntityId ?? null;
|
|
6509
|
+
const compatibilityRef = input.crmPersonRef ?? (entityId ? `XRay::entity::${entityId}` : `XRay::anonymous::${randomUUID2()}`);
|
|
6376
6510
|
const inserted = await client.query(
|
|
6377
|
-
`INSERT INTO analytics_people(id,
|
|
6378
|
-
|
|
6511
|
+
`INSERT INTO analytics_people(id,site_id,namespace_id,crm_person_ref,crm_entity_id,public_ref)
|
|
6512
|
+
VALUES($1,$2,$3,$4,$5::uuid,$6)
|
|
6513
|
+
ON CONFLICT(site_id,crm_person_ref) DO UPDATE SET last_seen_at=now(),
|
|
6379
6514
|
namespace_id=COALESCE(analytics_people.namespace_id,EXCLUDED.namespace_id),
|
|
6380
|
-
|
|
6381
|
-
|
|
6515
|
+
crm_entity_id=COALESCE(analytics_people.crm_entity_id,EXCLUDED.crm_entity_id),
|
|
6516
|
+
public_ref=COALESCE(analytics_people.public_ref,EXCLUDED.public_ref)
|
|
6517
|
+
RETURNING id`,
|
|
6518
|
+
[randomUUID2(), input.siteId, input.namespaceId, compatibilityRef, entityId, newAnalyticsPersonPublicRef()]
|
|
6382
6519
|
);
|
|
6383
|
-
|
|
6384
|
-
}
|
|
6385
|
-
await client.query(
|
|
6386
|
-
`UPDATE analytics_people SET last_seen_at=now(),
|
|
6387
|
-
namespace_id=COALESCE(namespace_id,$2),public_ref=COALESCE(public_ref,$3),
|
|
6388
|
-
crm_person_ref=CASE WHEN crm_person_ref LIKE 'XRay::anonymous::%' THEN $5 ELSE crm_person_ref END
|
|
6389
|
-
WHERE id=$1 AND site_id=$4`,
|
|
6390
|
-
[canonicalId, input.namespaceId, newAnalyticsPersonPublicRef(), input.siteId, input.crmPersonRef]
|
|
6391
|
-
);
|
|
6392
|
-
const duplicateIds = matches.rows.slice(1).map((row) => row.id);
|
|
6393
|
-
if (duplicateIds.length === 0) return { personId: canonicalId, mergedPersonIds: [] };
|
|
6394
|
-
await client.query(
|
|
6395
|
-
`DELETE FROM analytics_identity_edges duplicate
|
|
6396
|
-
USING analytics_identity_edges canonical
|
|
6397
|
-
WHERE duplicate.site_id=$1 AND duplicate.person_id=ANY($2::uuid[])
|
|
6398
|
-
AND canonical.site_id=duplicate.site_id AND canonical.person_id=$3
|
|
6399
|
-
AND canonical.identity_node_id=duplicate.identity_node_id
|
|
6400
|
-
AND canonical.evidence_kind=duplicate.evidence_kind`,
|
|
6401
|
-
[input.siteId, duplicateIds, canonicalId]
|
|
6402
|
-
);
|
|
6403
|
-
await client.query(
|
|
6404
|
-
`UPDATE analytics_identity_edges SET person_id=$3,namespace_id=$4
|
|
6405
|
-
WHERE site_id=$1 AND person_id=ANY($2::uuid[])`,
|
|
6406
|
-
[input.siteId, duplicateIds, canonicalId, input.namespaceId]
|
|
6407
|
-
);
|
|
6408
|
-
for (const table of [
|
|
6409
|
-
"analytics_form_submissions",
|
|
6410
|
-
"analytics_events",
|
|
6411
|
-
"analytics_conversions",
|
|
6412
|
-
"analytics_sessions",
|
|
6413
|
-
"analytics_attribution_touches",
|
|
6414
|
-
"analytics_external_events",
|
|
6415
|
-
"analytics_restricted_data",
|
|
6416
|
-
"analytics_crm_outbound_jobs"
|
|
6417
|
-
]) {
|
|
6520
|
+
personId = inserted.rows[0].id;
|
|
6521
|
+
} else {
|
|
6418
6522
|
await client.query(
|
|
6419
|
-
`UPDATE
|
|
6420
|
-
|
|
6523
|
+
`UPDATE analytics_people SET last_seen_at=now(),
|
|
6524
|
+
namespace_id=COALESCE(namespace_id,$2),
|
|
6525
|
+
crm_entity_id=COALESCE(crm_entity_id,$3::uuid),
|
|
6526
|
+
public_ref=COALESCE(public_ref,$4)
|
|
6527
|
+
WHERE id=$1 AND site_id=$5`,
|
|
6528
|
+
[personId, input.namespaceId, input.crmEntityId ?? null, newAnalyticsPersonPublicRef(), input.siteId]
|
|
6421
6529
|
);
|
|
6422
6530
|
}
|
|
6423
|
-
|
|
6424
|
-
|
|
6425
|
-
|
|
6426
|
-
|
|
6427
|
-
|
|
6428
|
-
|
|
6429
|
-
|
|
6430
|
-
|
|
6431
|
-
|
|
6432
|
-
|
|
6433
|
-
|
|
6434
|
-
|
|
6435
|
-
|
|
6436
|
-
|
|
6437
|
-
|
|
6438
|
-
|
|
6439
|
-
|
|
6440
|
-
|
|
6441
|
-
|
|
6442
|
-
|
|
6443
|
-
|
|
6444
|
-
|
|
6445
|
-
|
|
6446
|
-
|
|
6447
|
-
|
|
6448
|
-
|
|
6449
|
-
AND canonical.site_id=duplicate.site_id AND canonical.person_id=$3
|
|
6450
|
-
AND canonical.connection_id=duplicate.connection_id`,
|
|
6451
|
-
[input.siteId, duplicateIds, canonicalId]
|
|
6452
|
-
);
|
|
6453
|
-
await client.query(
|
|
6454
|
-
`UPDATE analytics_crm_person_mappings SET person_id=$3,namespace_id=$4
|
|
6455
|
-
WHERE site_id=$1 AND person_id=ANY($2::uuid[])`,
|
|
6456
|
-
[input.siteId, duplicateIds, canonicalId, input.namespaceId]
|
|
6457
|
-
);
|
|
6458
|
-
await client.query(
|
|
6459
|
-
`DELETE FROM analytics_prediction_results duplicate
|
|
6460
|
-
USING analytics_prediction_results canonical
|
|
6461
|
-
WHERE duplicate.site_id=$1 AND duplicate.person_id=ANY($2::uuid[])
|
|
6462
|
-
AND canonical.site_id=duplicate.site_id AND canonical.person_id=$3
|
|
6463
|
-
AND canonical.manifest_id=duplicate.manifest_id`,
|
|
6464
|
-
[input.siteId, duplicateIds, canonicalId]
|
|
6465
|
-
);
|
|
6466
|
-
for (const table of ["analytics_score_evaluations", "analytics_prediction_results"]) {
|
|
6531
|
+
return { personId, mergedPersonIds: [] };
|
|
6532
|
+
}
|
|
6533
|
+
async function assignAnalyticsIdentityNode(client, input) {
|
|
6534
|
+
const active = await client.query(
|
|
6535
|
+
`SELECT person_id,crm_entity_id::text FROM analytics_identity_assignments
|
|
6536
|
+
WHERE identity_node_id=$1 AND site_id=$2 FOR UPDATE`,
|
|
6537
|
+
[input.identityNodeId, input.siteId]
|
|
6538
|
+
);
|
|
6539
|
+
let assignment = active.rows[0];
|
|
6540
|
+
if (!assignment) {
|
|
6541
|
+
await client.query(
|
|
6542
|
+
`INSERT INTO analytics_identity_assignments(
|
|
6543
|
+
identity_node_id,site_id,person_id,crm_entity_id,evidence_kind
|
|
6544
|
+
) VALUES($1,$2,$3,$4::uuid,$5)
|
|
6545
|
+
ON CONFLICT(identity_node_id) DO NOTHING`,
|
|
6546
|
+
[input.identityNodeId, input.siteId, input.personId, input.crmEntityId ?? null, input.evidenceKind]
|
|
6547
|
+
);
|
|
6548
|
+
const winner = await client.query(
|
|
6549
|
+
`SELECT person_id FROM analytics_identity_assignments WHERE identity_node_id=$1 AND site_id=$2`,
|
|
6550
|
+
[input.identityNodeId, input.siteId]
|
|
6551
|
+
);
|
|
6552
|
+
if (winner.rows[0]?.person_id === input.personId) return "assigned";
|
|
6553
|
+
if (!winner.rows[0]) throw new AnalyticsRepositoryError("identity_assignment_unavailable", "Identity assignment could not be recorded.", 409);
|
|
6554
|
+
assignment = { person_id: winner.rows[0].person_id, crm_entity_id: null };
|
|
6555
|
+
}
|
|
6556
|
+
if (assignment.person_id === input.personId) {
|
|
6467
6557
|
await client.query(
|
|
6468
|
-
`UPDATE
|
|
6469
|
-
|
|
6470
|
-
|
|
6558
|
+
`UPDATE analytics_identity_assignments SET updated_at=now(),
|
|
6559
|
+
crm_entity_id=COALESCE(crm_entity_id,$3::uuid),evidence_kind=$4
|
|
6560
|
+
WHERE identity_node_id=$1 AND site_id=$2`,
|
|
6561
|
+
[input.identityNodeId, input.siteId, input.crmEntityId ?? null, input.evidenceKind]
|
|
6471
6562
|
);
|
|
6563
|
+
return "existing";
|
|
6472
6564
|
}
|
|
6473
6565
|
await client.query(
|
|
6474
|
-
|
|
6475
|
-
|
|
6476
|
-
|
|
6477
|
-
|
|
6478
|
-
|
|
6479
|
-
|
|
6480
|
-
|
|
6481
|
-
|
|
6482
|
-
|
|
6483
|
-
|
|
6566
|
+
`INSERT INTO analytics_identity_assignment_conflicts(
|
|
6567
|
+
id,site_id,identity_node_id,active_person_id,attempted_person_id,
|
|
6568
|
+
active_crm_entity_id,attempted_crm_entity_id,evidence_kind
|
|
6569
|
+
) VALUES($1,$2,$3,$4,$5,$6::uuid,$7::uuid,$8)
|
|
6570
|
+
ON CONFLICT(identity_node_id,active_person_id,attempted_person_id)
|
|
6571
|
+
WHERE status='pending' DO UPDATE SET observed_at=now(),evidence_kind=EXCLUDED.evidence_kind`,
|
|
6572
|
+
[
|
|
6573
|
+
randomUUID2(),
|
|
6574
|
+
input.siteId,
|
|
6575
|
+
input.identityNodeId,
|
|
6576
|
+
assignment.person_id,
|
|
6577
|
+
input.personId,
|
|
6578
|
+
assignment.crm_entity_id,
|
|
6579
|
+
input.crmEntityId ?? null,
|
|
6580
|
+
input.evidenceKind
|
|
6581
|
+
]
|
|
6484
6582
|
);
|
|
6485
|
-
return
|
|
6583
|
+
return "conflict";
|
|
6486
6584
|
}
|
|
6487
6585
|
async function promoteAnalyticsCandidatesForConfirmedPerson(client, input) {
|
|
6488
6586
|
const associations = await client.query(
|
|
@@ -6632,6 +6730,7 @@ async function linkAnalyticsIdentityInTransaction(client, input) {
|
|
|
6632
6730
|
for (const [platform, clickId] of Object.entries(sanitizeClickIds(input.clickIds)))
|
|
6633
6731
|
signals.push({ kind: "click_id", value: `${platform}:${clickId}`, evidence: platform, confidence: 0.9 });
|
|
6634
6732
|
const identityNodeIds = [];
|
|
6733
|
+
let hasAssignmentConflict = false;
|
|
6635
6734
|
for (const signal of signals) {
|
|
6636
6735
|
const node = await client.query(
|
|
6637
6736
|
`INSERT INTO analytics_identity_nodes(id, site_id, namespace_id, kind, value_hmac) VALUES ($1,$2,$3,$4,$5)
|
|
@@ -6648,8 +6747,15 @@ async function linkAnalyticsIdentityInTransaction(client, input) {
|
|
|
6648
6747
|
namespace_id=COALESCE(analytics_identity_edges.namespace_id,EXCLUDED.namespace_id)`,
|
|
6649
6748
|
[randomUUID2(), input.siteId, namespaceId, personId, node.rows[0].id, signal.evidence, signal.confidence]
|
|
6650
6749
|
);
|
|
6750
|
+
const assignment = await assignAnalyticsIdentityNode(client, {
|
|
6751
|
+
siteId: input.siteId,
|
|
6752
|
+
personId,
|
|
6753
|
+
identityNodeId: node.rows[0].id,
|
|
6754
|
+
evidenceKind: signal.evidence
|
|
6755
|
+
});
|
|
6756
|
+
hasAssignmentConflict ||= assignment === "conflict";
|
|
6651
6757
|
}
|
|
6652
|
-
await completeAnalyticsIdentityPromotion(client, {
|
|
6758
|
+
if (!hasAssignmentConflict) await completeAnalyticsIdentityPromotion(client, {
|
|
6653
6759
|
siteId: input.siteId,
|
|
6654
6760
|
namespaceId,
|
|
6655
6761
|
personId,
|
|
@@ -6662,7 +6768,7 @@ async function linkAnalyticsIdentityInTransaction(client, input) {
|
|
|
6662
6768
|
sessionId: input.sessionId,
|
|
6663
6769
|
occurredAt: input.occurredAt
|
|
6664
6770
|
});
|
|
6665
|
-
if (input.customerId) {
|
|
6771
|
+
if (input.customerId && !hasAssignmentConflict) {
|
|
6666
6772
|
await writeAnalyticsConfirmedIdentityProfile(client, {
|
|
6667
6773
|
siteId: input.siteId,
|
|
6668
6774
|
personId,
|
|
@@ -6685,7 +6791,7 @@ async function linkAnalyticsFormIdentity(input) {
|
|
|
6685
6791
|
input.sessionId
|
|
6686
6792
|
)) {
|
|
6687
6793
|
await client.query("COMMIT");
|
|
6688
|
-
return { personId: null, linkedSignals: 0 };
|
|
6794
|
+
return { personId: null, linkedSignals: 0, conflicts: 0 };
|
|
6689
6795
|
}
|
|
6690
6796
|
const namespaceId = await ensurePrimaryAnalyticsIdentityNamespace(client, input.siteId, "form");
|
|
6691
6797
|
const deterministicSignals = [
|
|
@@ -6705,6 +6811,7 @@ async function linkAnalyticsFormIdentity(input) {
|
|
|
6705
6811
|
siteId: input.siteId,
|
|
6706
6812
|
namespaceId,
|
|
6707
6813
|
crmPersonRef: input.crmPersonRef,
|
|
6814
|
+
crmEntityId: input.crmEntityId,
|
|
6708
6815
|
signals: matchSignals
|
|
6709
6816
|
});
|
|
6710
6817
|
const personId = resolution.personId;
|
|
@@ -6748,6 +6855,7 @@ async function linkAnalyticsFormIdentity(input) {
|
|
|
6748
6855
|
});
|
|
6749
6856
|
}
|
|
6750
6857
|
const identityNodeIds = [];
|
|
6858
|
+
let conflicts = 0;
|
|
6751
6859
|
for (const signal of signals) {
|
|
6752
6860
|
const node = await client.query(
|
|
6753
6861
|
`INSERT INTO analytics_identity_nodes(id, site_id, namespace_id, kind, value_hmac) VALUES ($1,$2,$3,$4,$5)
|
|
@@ -6778,25 +6886,33 @@ async function linkAnalyticsFormIdentity(input) {
|
|
|
6778
6886
|
signal.confidence
|
|
6779
6887
|
]
|
|
6780
6888
|
);
|
|
6889
|
+
const assignment = await assignAnalyticsIdentityNode(client, {
|
|
6890
|
+
siteId: input.siteId,
|
|
6891
|
+
personId,
|
|
6892
|
+
crmEntityId: input.crmEntityId,
|
|
6893
|
+
identityNodeId: node.rows[0].id,
|
|
6894
|
+
evidenceKind: signal.evidence
|
|
6895
|
+
});
|
|
6896
|
+
if (assignment === "conflict") conflicts += 1;
|
|
6781
6897
|
}
|
|
6782
6898
|
if (input.submissionId)
|
|
6783
6899
|
await client.query(
|
|
6784
6900
|
"UPDATE analytics_form_submissions SET person_id = $1 WHERE id = $2 AND site_id = $3",
|
|
6785
6901
|
[personId, input.submissionId, input.siteId]
|
|
6786
6902
|
);
|
|
6787
|
-
await completeAnalyticsIdentityPromotion(client, {
|
|
6903
|
+
if (conflicts === 0) await completeAnalyticsIdentityPromotion(client, {
|
|
6788
6904
|
siteId: input.siteId,
|
|
6789
6905
|
namespaceId,
|
|
6790
6906
|
personId,
|
|
6791
6907
|
evidenceKind: "form_fill",
|
|
6792
6908
|
candidateEvidenceKind: input.email ? "verified_email" : input.phone ? "verified_phone" : "authenticated_user",
|
|
6793
|
-
evidenceRef: input.submissionId ?? input.crmPersonRef,
|
|
6909
|
+
evidenceRef: input.submissionId ?? input.crmEntityId ?? input.crmPersonRef ?? personId,
|
|
6794
6910
|
priorPersonIds: resolution.mergedPersonIds,
|
|
6795
6911
|
priorIdentityNodeIds: identityNodeIds,
|
|
6796
6912
|
visitorId: input.visitorId,
|
|
6797
6913
|
sessionId: input.sessionId
|
|
6798
6914
|
});
|
|
6799
|
-
await writeAnalyticsConfirmedIdentityProfile(client, {
|
|
6915
|
+
if (conflicts === 0) await writeAnalyticsConfirmedIdentityProfile(client, {
|
|
6800
6916
|
siteId: input.siteId,
|
|
6801
6917
|
personId,
|
|
6802
6918
|
consentSubjectRefs: [input.visitorId ?? ""],
|
|
@@ -6806,7 +6922,7 @@ async function linkAnalyticsFormIdentity(input) {
|
|
|
6806
6922
|
phone: input.phone
|
|
6807
6923
|
});
|
|
6808
6924
|
await client.query("COMMIT");
|
|
6809
|
-
return { personId, linkedSignals: signals.length };
|
|
6925
|
+
return { personId, linkedSignals: signals.length, conflicts };
|
|
6810
6926
|
} catch (error) {
|
|
6811
6927
|
await client.query("ROLLBACK");
|
|
6812
6928
|
throw error;
|
|
@@ -6827,7 +6943,7 @@ async function enrichAnalyticsExistingCrmIdentity(input) {
|
|
|
6827
6943
|
const person = existing.rows[0];
|
|
6828
6944
|
if (!person || !await hasCurrentPersonIdentityConsent(client, input.siteId, person.public_ref)) {
|
|
6829
6945
|
await client.query("COMMIT");
|
|
6830
|
-
return { personId: null, linkedSignals: 0 };
|
|
6946
|
+
return { personId: null, linkedSignals: 0, conflicts: 0 };
|
|
6831
6947
|
}
|
|
6832
6948
|
const signals = [
|
|
6833
6949
|
...input.email ? [{ kind: "email", value: input.email, confidence: 1 }] : [],
|
|
@@ -6841,6 +6957,7 @@ async function enrichAnalyticsExistingCrmIdentity(input) {
|
|
|
6841
6957
|
siteId: input.siteId,
|
|
6842
6958
|
namespaceId: person.namespace_id,
|
|
6843
6959
|
crmPersonRef: input.crmPersonRef,
|
|
6960
|
+
crmEntityId: input.crmEntityId,
|
|
6844
6961
|
signals: signals.map((signal) => ({ kind: signal.kind, valueHmac: identityHmac(signal.kind, signal.value) }))
|
|
6845
6962
|
});
|
|
6846
6963
|
const personId = resolution.personId;
|
|
@@ -6850,6 +6967,7 @@ async function enrichAnalyticsExistingCrmIdentity(input) {
|
|
|
6850
6967
|
);
|
|
6851
6968
|
const canonicalPublicRef = canonical.rows[0]?.public_ref ?? person.public_ref;
|
|
6852
6969
|
const identityNodeIds = [];
|
|
6970
|
+
let conflicts = 0;
|
|
6853
6971
|
for (const signal of signals) {
|
|
6854
6972
|
const node = await client.query(
|
|
6855
6973
|
`INSERT INTO analytics_identity_nodes(id,site_id,namespace_id,kind,value_hmac) VALUES($1,$2,$3,$4,$5)
|
|
@@ -6865,8 +6983,16 @@ async function enrichAnalyticsExistingCrmIdentity(input) {
|
|
|
6865
6983
|
confidence=greatest(analytics_identity_edges.confidence,EXCLUDED.confidence)`,
|
|
6866
6984
|
[randomUUID2(), input.siteId, person.namespace_id, personId, node.rows[0].id, signal.confidence]
|
|
6867
6985
|
);
|
|
6986
|
+
const assignment = await assignAnalyticsIdentityNode(client, {
|
|
6987
|
+
siteId: input.siteId,
|
|
6988
|
+
personId,
|
|
6989
|
+
crmEntityId: input.crmEntityId,
|
|
6990
|
+
identityNodeId: node.rows[0].id,
|
|
6991
|
+
evidenceKind: "crm_contact"
|
|
6992
|
+
});
|
|
6993
|
+
if (assignment === "conflict") conflicts += 1;
|
|
6868
6994
|
}
|
|
6869
|
-
await completeAnalyticsIdentityPromotion(client, {
|
|
6995
|
+
if (conflicts === 0) await completeAnalyticsIdentityPromotion(client, {
|
|
6870
6996
|
siteId: input.siteId,
|
|
6871
6997
|
namespaceId: person.namespace_id,
|
|
6872
6998
|
personId,
|
|
@@ -6876,7 +7002,7 @@ async function enrichAnalyticsExistingCrmIdentity(input) {
|
|
|
6876
7002
|
priorPersonIds: resolution.mergedPersonIds,
|
|
6877
7003
|
priorIdentityNodeIds: identityNodeIds
|
|
6878
7004
|
});
|
|
6879
|
-
await writeAnalyticsConfirmedIdentityProfile(client, {
|
|
7005
|
+
if (conflicts === 0) await writeAnalyticsConfirmedIdentityProfile(client, {
|
|
6880
7006
|
siteId: input.siteId,
|
|
6881
7007
|
personId,
|
|
6882
7008
|
consentSubjectRefs: [canonicalPublicRef],
|
|
@@ -6886,7 +7012,7 @@ async function enrichAnalyticsExistingCrmIdentity(input) {
|
|
|
6886
7012
|
phone: input.phone
|
|
6887
7013
|
});
|
|
6888
7014
|
await client.query("COMMIT");
|
|
6889
|
-
return { personId, linkedSignals: signals.length };
|
|
7015
|
+
return { personId, linkedSignals: signals.length, conflicts };
|
|
6890
7016
|
} catch (error) {
|
|
6891
7017
|
await client.query("ROLLBACK");
|
|
6892
7018
|
throw error;
|
|
@@ -7533,6 +7659,111 @@ async function queueAnalyticsFormDelivery(input) {
|
|
|
7533
7659
|
);
|
|
7534
7660
|
return id;
|
|
7535
7661
|
}
|
|
7662
|
+
async function queueAnalyticsFormBridgeTransaction(input) {
|
|
7663
|
+
const client = await getAnalyticsPool().connect();
|
|
7664
|
+
const proposedSubmissionId = randomUUID2();
|
|
7665
|
+
const proposedBridgeRunId = randomUUID2();
|
|
7666
|
+
const proposedCrmEntityId = input.crmEntityId ?? randomUUID2();
|
|
7667
|
+
const occurredAt = input.occurredAt ?? (/* @__PURE__ */ new Date()).toISOString();
|
|
7668
|
+
try {
|
|
7669
|
+
await client.query("BEGIN");
|
|
7670
|
+
const inserted = await client.query(
|
|
7671
|
+
`INSERT INTO analytics_form_submissions(
|
|
7672
|
+
id,form_id,site_id,pixel_id,visitor_id,session_id,crm_person_ref,
|
|
7673
|
+
crm_entity_id,bridge_run_id,idempotency_key,source,medium,campaign,
|
|
7674
|
+
crm_delivery_status,click_ids,occurred_at
|
|
7675
|
+
) VALUES($1,$2,$3,$4,$5,$6,$7,$8::uuid,$9,$10,$11,$12,$13,'pending',$14::jsonb,$15)
|
|
7676
|
+
ON CONFLICT(form_id,idempotency_key) WHERE idempotency_key IS NOT NULL DO NOTHING
|
|
7677
|
+
RETURNING id,bridge_run_id,crm_entity_id::text`,
|
|
7678
|
+
[
|
|
7679
|
+
proposedSubmissionId,
|
|
7680
|
+
input.form.id,
|
|
7681
|
+
input.form.site_id,
|
|
7682
|
+
input.form.pixel_id,
|
|
7683
|
+
input.visitorId ?? null,
|
|
7684
|
+
input.sessionId ?? null,
|
|
7685
|
+
input.legacyCrmPersonRef ?? null,
|
|
7686
|
+
proposedCrmEntityId,
|
|
7687
|
+
proposedBridgeRunId,
|
|
7688
|
+
input.idempotencyKey,
|
|
7689
|
+
input.source?.slice(0, 180) ?? null,
|
|
7690
|
+
input.medium?.slice(0, 180) ?? null,
|
|
7691
|
+
input.campaign?.slice(0, 240) ?? null,
|
|
7692
|
+
JSON.stringify(sanitizeClickIds(input.clickIds)),
|
|
7693
|
+
occurredAt
|
|
7694
|
+
]
|
|
7695
|
+
);
|
|
7696
|
+
const created = Boolean(inserted.rows[0]);
|
|
7697
|
+
const existing = inserted.rows[0] ?? (await client.query(
|
|
7698
|
+
`SELECT id,bridge_run_id::text,crm_entity_id::text
|
|
7699
|
+
FROM analytics_form_submissions
|
|
7700
|
+
WHERE form_id=$1 AND idempotency_key=$2 FOR UPDATE`,
|
|
7701
|
+
[input.form.id, input.idempotencyKey]
|
|
7702
|
+
)).rows[0];
|
|
7703
|
+
if (!existing?.bridge_run_id || !existing.crm_entity_id)
|
|
7704
|
+
throw new AnalyticsRepositoryError(
|
|
7705
|
+
"form_bridge_idempotency_conflict",
|
|
7706
|
+
"The form idempotency receipt belongs to a legacy submission.",
|
|
7707
|
+
409
|
|
7708
|
+
);
|
|
7709
|
+
const conversionId = randomUUID2();
|
|
7710
|
+
const conversion = await client.query(
|
|
7711
|
+
`INSERT INTO analytics_conversions(
|
|
7712
|
+
id,source_event_id,site_id,pixel_id,session_id,session_hmac,
|
|
7713
|
+
conversion_kind,value_minor,currency,occurred_at
|
|
7714
|
+
) VALUES($1,$2,$3,$4,$5,$6,'form_submitted',0,'USD',$7)
|
|
7715
|
+
ON CONFLICT(site_id,source_event_id) DO UPDATE SET source_event_id=EXCLUDED.source_event_id
|
|
7716
|
+
RETURNING id`,
|
|
7717
|
+
[
|
|
7718
|
+
conversionId,
|
|
7719
|
+
`form:${existing.id}`,
|
|
7720
|
+
input.form.site_id,
|
|
7721
|
+
input.form.pixel_id,
|
|
7722
|
+
input.sessionId ?? null,
|
|
7723
|
+
input.sessionId ? identityHmac("session_id", input.sessionId) : null,
|
|
7724
|
+
occurredAt
|
|
7725
|
+
]
|
|
7726
|
+
);
|
|
7727
|
+
await client.query(
|
|
7728
|
+
`INSERT INTO analytics_form_delivery_jobs(
|
|
7729
|
+
id,submission_id,owner_user_id,payload_ciphertext,bridge_run_id,
|
|
7730
|
+
crm_entity_id,idempotency_key
|
|
7731
|
+
) VALUES($1,$2,$3,$4,$5,$6::uuid,$7)
|
|
7732
|
+
ON CONFLICT(submission_id) DO NOTHING`,
|
|
7733
|
+
[
|
|
7734
|
+
randomUUID2(),
|
|
7735
|
+
existing.id,
|
|
7736
|
+
input.ownerUserId,
|
|
7737
|
+
input.payloadCiphertext,
|
|
7738
|
+
existing.bridge_run_id,
|
|
7739
|
+
existing.crm_entity_id,
|
|
7740
|
+
input.idempotencyKey
|
|
7741
|
+
]
|
|
7742
|
+
);
|
|
7743
|
+
await client.query(
|
|
7744
|
+
`INSERT INTO analytics_form_bridge_receipts(
|
|
7745
|
+
id,bridge_run_id,submission_id,system,status,opaque_entity_id
|
|
7746
|
+
) VALUES
|
|
7747
|
+
($1,$2,$3,'analytics','delivered',NULL),
|
|
7748
|
+
($4,$2,$3,'crm','pending',$5::uuid)
|
|
7749
|
+
ON CONFLICT(bridge_run_id,system) DO NOTHING`,
|
|
7750
|
+
[randomUUID2(), existing.bridge_run_id, existing.id, randomUUID2(), existing.crm_entity_id]
|
|
7751
|
+
);
|
|
7752
|
+
await client.query("COMMIT");
|
|
7753
|
+
return {
|
|
7754
|
+
submissionId: existing.id,
|
|
7755
|
+
bridgeRunId: existing.bridge_run_id,
|
|
7756
|
+
crmEntityId: existing.crm_entity_id,
|
|
7757
|
+
conversionId: conversion.rows[0].id,
|
|
7758
|
+
created
|
|
7759
|
+
};
|
|
7760
|
+
} catch (error) {
|
|
7761
|
+
await client.query("ROLLBACK");
|
|
7762
|
+
throw error;
|
|
7763
|
+
} finally {
|
|
7764
|
+
client.release();
|
|
7765
|
+
}
|
|
7766
|
+
}
|
|
7536
7767
|
async function sweepAnalyticsRestrictedRetention() {
|
|
7537
7768
|
const db = getAnalyticsPool();
|
|
7538
7769
|
const client = await db.connect();
|
|
@@ -7575,7 +7806,8 @@ async function claimAnalyticsFormDeliveryJobs(limit = 10) {
|
|
|
7575
7806
|
SET attempts = j.attempts + 1,
|
|
7576
7807
|
next_attempt_at = now() + interval '5 minutes'
|
|
7577
7808
|
FROM due WHERE j.id = due.id
|
|
7578
|
-
RETURNING j.id, j.submission_id, j.owner_user_id, j.payload_ciphertext, j.attempts
|
|
7809
|
+
RETURNING j.id, j.submission_id, j.owner_user_id, j.payload_ciphertext, j.attempts,
|
|
7810
|
+
j.bridge_run_id::text,j.crm_entity_id::text,j.idempotency_key`,
|
|
7579
7811
|
[Math.min(Math.max(limit, 1), 50)]
|
|
7580
7812
|
);
|
|
7581
7813
|
return result.rows.map((row) => ({
|
|
@@ -7583,21 +7815,37 @@ async function claimAnalyticsFormDeliveryJobs(limit = 10) {
|
|
|
7583
7815
|
submissionId: row.submission_id,
|
|
7584
7816
|
ownerUserId: Number(row.owner_user_id),
|
|
7585
7817
|
payloadCiphertext: row.payload_ciphertext,
|
|
7586
|
-
attempts: row.attempts
|
|
7818
|
+
attempts: row.attempts,
|
|
7819
|
+
bridgeRunId: row.bridge_run_id,
|
|
7820
|
+
crmEntityId: row.crm_entity_id,
|
|
7821
|
+
idempotencyKey: row.idempotency_key
|
|
7587
7822
|
}));
|
|
7588
7823
|
}
|
|
7589
7824
|
async function completeAnalyticsFormDelivery(jobId, submissionId) {
|
|
7825
|
+
await completeAnalyticsFormBridgeDelivery({ jobId, submissionId });
|
|
7826
|
+
}
|
|
7827
|
+
async function completeAnalyticsFormBridgeDelivery(input) {
|
|
7590
7828
|
const db = getAnalyticsPool();
|
|
7591
7829
|
const client = await db.connect();
|
|
7592
7830
|
try {
|
|
7593
7831
|
await client.query("BEGIN");
|
|
7594
7832
|
await client.query(
|
|
7595
7833
|
`UPDATE analytics_form_delivery_jobs SET status='delivered', delivered_at=now(), last_error_code=NULL WHERE id=$1`,
|
|
7596
|
-
[jobId]
|
|
7834
|
+
[input.jobId]
|
|
7597
7835
|
);
|
|
7598
7836
|
await client.query(
|
|
7599
|
-
`UPDATE analytics_form_submissions SET crm_delivery_status='delivered'
|
|
7600
|
-
|
|
7837
|
+
`UPDATE analytics_form_submissions SET crm_delivery_status='delivered',
|
|
7838
|
+
crm_entity_id=COALESCE(crm_entity_id,$2::uuid) WHERE id=$1`,
|
|
7839
|
+
[input.submissionId, input.crmEntityId ?? null]
|
|
7840
|
+
);
|
|
7841
|
+
await client.query(
|
|
7842
|
+
`UPDATE analytics_form_bridge_receipts receipt
|
|
7843
|
+
SET status='delivered',opaque_entity_id=COALESCE(opaque_entity_id,$3::uuid),
|
|
7844
|
+
safe_error_code=NULL,updated_at=now()
|
|
7845
|
+
FROM analytics_form_delivery_jobs job
|
|
7846
|
+
WHERE job.id=$1 AND receipt.bridge_run_id=job.bridge_run_id
|
|
7847
|
+
AND receipt.submission_id=$2 AND receipt.system='crm'`,
|
|
7848
|
+
[input.jobId, input.submissionId, input.crmEntityId ?? null]
|
|
7601
7849
|
);
|
|
7602
7850
|
await client.query("COMMIT");
|
|
7603
7851
|
} catch (error) {
|
|
@@ -7610,14 +7858,34 @@ async function completeAnalyticsFormDelivery(jobId, submissionId) {
|
|
|
7610
7858
|
async function deferAnalyticsFormDelivery(jobId, attempts, errorCode) {
|
|
7611
7859
|
const terminal = attempts >= 8;
|
|
7612
7860
|
const delayMinutes = Math.min(5 * 2 ** Math.max(0, attempts - 1), 360);
|
|
7613
|
-
await getAnalyticsPool().
|
|
7614
|
-
|
|
7615
|
-
|
|
7616
|
-
|
|
7617
|
-
|
|
7618
|
-
|
|
7619
|
-
|
|
7620
|
-
|
|
7861
|
+
const client = await getAnalyticsPool().connect();
|
|
7862
|
+
try {
|
|
7863
|
+
await client.query("BEGIN");
|
|
7864
|
+
const updated = await client.query(
|
|
7865
|
+
`UPDATE analytics_form_delivery_jobs
|
|
7866
|
+
SET status = CASE WHEN expires_at <= now() THEN 'expired' WHEN $2 THEN 'failed' ELSE 'pending' END,
|
|
7867
|
+
last_error_code = $3,
|
|
7868
|
+
next_attempt_at = now() + ($4::text || ' minutes')::interval
|
|
7869
|
+
WHERE id = $1
|
|
7870
|
+
RETURNING bridge_run_id::text,submission_id::text,status`,
|
|
7871
|
+
[jobId, terminal, errorCode.slice(0, 80), String(delayMinutes)]
|
|
7872
|
+
);
|
|
7873
|
+
const job = updated.rows[0];
|
|
7874
|
+
if (job?.bridge_run_id) {
|
|
7875
|
+
await client.query(
|
|
7876
|
+
`UPDATE analytics_form_bridge_receipts SET status=$3,
|
|
7877
|
+
safe_error_code=$4,updated_at=now()
|
|
7878
|
+
WHERE bridge_run_id=$1 AND submission_id=$2 AND system='crm'`,
|
|
7879
|
+
[job.bridge_run_id, job.submission_id, job.status, errorCode.slice(0, 80)]
|
|
7880
|
+
);
|
|
7881
|
+
}
|
|
7882
|
+
await client.query("COMMIT");
|
|
7883
|
+
} catch (error) {
|
|
7884
|
+
await client.query("ROLLBACK");
|
|
7885
|
+
throw error;
|
|
7886
|
+
} finally {
|
|
7887
|
+
client.release();
|
|
7888
|
+
}
|
|
7621
7889
|
}
|
|
7622
7890
|
async function isAnalyticsFormPlacementApproved(pixelId, placementUrl) {
|
|
7623
7891
|
if (!placementUrl) return false;
|
|
@@ -7983,7 +8251,9 @@ export {
|
|
|
7983
8251
|
recordAnalyticsFormSubmission,
|
|
7984
8252
|
sanitizeClickIds,
|
|
7985
8253
|
identityHmac,
|
|
8254
|
+
deterministicAnalyticsCrmEntityId,
|
|
7986
8255
|
backfillAnalyticsConfirmedHistory,
|
|
8256
|
+
assignAnalyticsIdentityNode,
|
|
7987
8257
|
linkAnalyticsIdentityInTransaction,
|
|
7988
8258
|
linkAnalyticsFormIdentity,
|
|
7989
8259
|
enrichAnalyticsExistingCrmIdentity,
|
|
@@ -8005,9 +8275,11 @@ export {
|
|
|
8005
8275
|
pollAnalyticsActivationDiagnostics,
|
|
8006
8276
|
queueAnalyticsActivation,
|
|
8007
8277
|
queueAnalyticsFormDelivery,
|
|
8278
|
+
queueAnalyticsFormBridgeTransaction,
|
|
8008
8279
|
sweepAnalyticsRestrictedRetention,
|
|
8009
8280
|
claimAnalyticsFormDeliveryJobs,
|
|
8010
8281
|
completeAnalyticsFormDelivery,
|
|
8282
|
+
completeAnalyticsFormBridgeDelivery,
|
|
8011
8283
|
deferAnalyticsFormDelivery,
|
|
8012
8284
|
isAnalyticsFormPlacementApproved,
|
|
8013
8285
|
analyticsHealth,
|