@enricai/barnacle 1.6.10 → 1.6.12

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.
Files changed (93) hide show
  1. package/README.md +71 -3
  2. package/dist/api/routes/submissions.d.ts +21 -0
  3. package/dist/api/routes/submissions.d.ts.map +1 -0
  4. package/dist/api/routes/submissions.js +48 -0
  5. package/dist/api/routes/submissions.js.map +1 -0
  6. package/dist/api/schemas/submissions.d.ts +110 -0
  7. package/dist/api/schemas/submissions.d.ts.map +1 -0
  8. package/dist/api/schemas/submissions.js +77 -0
  9. package/dist/api/schemas/submissions.js.map +1 -0
  10. package/dist/config.d.ts +34 -0
  11. package/dist/config.d.ts.map +1 -1
  12. package/dist/config.js +5 -0
  13. package/dist/config.js.map +1 -1
  14. package/dist/lib/telemetry/beacon-capture.d.ts +37 -0
  15. package/dist/lib/telemetry/beacon-capture.d.ts.map +1 -0
  16. package/dist/lib/telemetry/beacon-capture.js +47 -0
  17. package/dist/lib/telemetry/beacon-capture.js.map +1 -0
  18. package/dist/lib/telemetry/reconciliation-record.d.ts +113 -0
  19. package/dist/lib/telemetry/reconciliation-record.d.ts.map +1 -0
  20. package/dist/lib/telemetry/reconciliation-record.js +102 -0
  21. package/dist/lib/telemetry/reconciliation-record.js.map +1 -0
  22. package/dist/lib/telemetry/reconciliation-source.d.ts +33 -0
  23. package/dist/lib/telemetry/reconciliation-source.d.ts.map +1 -0
  24. package/dist/lib/telemetry/reconciliation-source.js +91 -0
  25. package/dist/lib/telemetry/reconciliation-source.js.map +1 -0
  26. package/dist/lib/telemetry/s3-sink.d.ts.map +1 -1
  27. package/dist/lib/telemetry/s3-sink.js +1 -2
  28. package/dist/lib/telemetry/s3-sink.js.map +1 -1
  29. package/dist/lib/telemetry/submission-capture.d.ts +30 -9
  30. package/dist/lib/telemetry/submission-capture.d.ts.map +1 -1
  31. package/dist/lib/telemetry/submission-capture.js +32 -21
  32. package/dist/lib/telemetry/submission-capture.js.map +1 -1
  33. package/dist/lib/telemetry/submission-query.d.ts +32 -0
  34. package/dist/lib/telemetry/submission-query.d.ts.map +1 -0
  35. package/dist/lib/telemetry/submission-query.js +49 -0
  36. package/dist/lib/telemetry/submission-query.js.map +1 -0
  37. package/dist/lib/telemetry/submission-reader.d.ts +52 -0
  38. package/dist/lib/telemetry/submission-reader.d.ts.map +1 -0
  39. package/dist/lib/telemetry/submission-reader.js +111 -0
  40. package/dist/lib/telemetry/submission-reader.js.map +1 -0
  41. package/dist/lib/telemetry/submissions-s3-objects.d.ts +23 -0
  42. package/dist/lib/telemetry/submissions-s3-objects.d.ts.map +1 -0
  43. package/dist/lib/telemetry/submissions-s3-objects.js +132 -0
  44. package/dist/lib/telemetry/submissions-s3-objects.js.map +1 -0
  45. package/dist/lib/telemetry/submissions-s3-reader.d.ts +25 -0
  46. package/dist/lib/telemetry/submissions-s3-reader.d.ts.map +1 -0
  47. package/dist/lib/telemetry/submissions-s3-reader.js +63 -0
  48. package/dist/lib/telemetry/submissions-s3-reader.js.map +1 -0
  49. package/dist/lib/tracking-click.d.ts +13 -2
  50. package/dist/lib/tracking-click.d.ts.map +1 -1
  51. package/dist/lib/tracking-click.js +42 -4
  52. package/dist/lib/tracking-click.js.map +1 -1
  53. package/dist/plugins/loader.d.ts +17 -3
  54. package/dist/plugins/loader.d.ts.map +1 -1
  55. package/dist/plugins/loader.js +59 -6
  56. package/dist/plugins/loader.js.map +1 -1
  57. package/dist/scraper/cookie-jar.d.ts +10 -4
  58. package/dist/scraper/cookie-jar.d.ts.map +1 -1
  59. package/dist/scraper/cookie-jar.js +10 -5
  60. package/dist/scraper/cookie-jar.js.map +1 -1
  61. package/dist/scraper/deep-locator-candidates.d.ts +27 -5
  62. package/dist/scraper/deep-locator-candidates.d.ts.map +1 -1
  63. package/dist/scraper/deep-locator-candidates.js +57 -11
  64. package/dist/scraper/deep-locator-candidates.js.map +1 -1
  65. package/dist/scraper/deep-locator-fake.d.ts +28 -0
  66. package/dist/scraper/deep-locator-fake.d.ts.map +1 -1
  67. package/dist/scraper/deep-locator-fake.js +44 -2
  68. package/dist/scraper/deep-locator-fake.js.map +1 -1
  69. package/dist/scraper/flow-runner.d.ts +20 -0
  70. package/dist/scraper/flow-runner.d.ts.map +1 -1
  71. package/dist/scraper/flow-runner.js +81 -9
  72. package/dist/scraper/flow-runner.js.map +1 -1
  73. package/dist/scraper/frame-target.d.ts +60 -14
  74. package/dist/scraper/frame-target.d.ts.map +1 -1
  75. package/dist/scraper/frame-target.js +126 -38
  76. package/dist/scraper/frame-target.js.map +1 -1
  77. package/dist/scraper/stagehand-guard.d.ts +14 -0
  78. package/dist/scraper/stagehand-guard.d.ts.map +1 -1
  79. package/dist/scraper/stagehand-guard.js +28 -8
  80. package/dist/scraper/stagehand-guard.js.map +1 -1
  81. package/dist/scraper/watchdog.d.ts +38 -0
  82. package/dist/scraper/watchdog.d.ts.map +1 -0
  83. package/dist/scraper/watchdog.js +54 -0
  84. package/dist/scraper/watchdog.js.map +1 -0
  85. package/dist/scripts/recon-browser.d.ts.map +1 -1
  86. package/dist/scripts/recon-browser.js +14 -3
  87. package/dist/scripts/recon-browser.js.map +1 -1
  88. package/dist/server.d.ts.map +1 -1
  89. package/dist/server.js +2 -0
  90. package/dist/server.js.map +1 -1
  91. package/dist/site-plugin.d.ts +12 -0
  92. package/dist/site-plugin.d.ts.map +1 -1
  93. package/package.json +1 -1
@@ -0,0 +1,49 @@
1
+ "use strict";
2
+ /**
3
+ * Predicate composition over `ReconciliationRow[]` (feat-007's reader
4
+ * output), so a caller can ask reconciliation-shaped questions — rows for a
5
+ * given `siteId` cohort, `requestId`, submit outcome, beacon status, or date
6
+ * window — without re-parsing the submissions NDJSON. Pure and I/O-free: the
7
+ * route layer composes `readDurableReconciliationRows`
8
+ * (`src/lib/telemetry/reconciliation-source.ts`) then this module.
9
+ */
10
+ Object.defineProperty(exports, "__esModule", { value: true });
11
+ exports.queryReconciliationRows = queryReconciliationRows;
12
+ const date_fns_1 = require("date-fns");
13
+ /**
14
+ * Builds the AND-composed predicate list for the filter fields actually
15
+ * present, so an absent field never excludes a row. `from`/`to` are
16
+ * inclusive bounds expressed via `isBefore`/`isAfter` negation rather than
17
+ * `isWithinInterval`, since either bound may be supplied without the other.
18
+ */
19
+ function buildPredicates(filter) {
20
+ const { siteId, requestId, status, beaconStatus, from, to } = filter;
21
+ return [
22
+ siteId === undefined ? null : (row) => row.siteId === siteId,
23
+ requestId === undefined ? null : (row) => row.requestId === requestId,
24
+ status === undefined ? null : (row) => row.status === status,
25
+ beaconStatus === undefined
26
+ ? null
27
+ : (row) => row.beaconStatus === beaconStatus,
28
+ from === undefined
29
+ ? null
30
+ : (row) => !(0, date_fns_1.isBefore)((0, date_fns_1.parseISO)(row.ts), (0, date_fns_1.parseISO)(from)),
31
+ to === undefined ? null : (row) => !(0, date_fns_1.isAfter)((0, date_fns_1.parseISO)(row.ts), (0, date_fns_1.parseISO)(to)),
32
+ ].filter((predicate) => predicate !== null);
33
+ }
34
+ /**
35
+ * Filters, sorts (newest-first by `ts`), and paginates reconciliation rows
36
+ * so the attribution-report join a human pulls in pages is deterministic
37
+ * rather than relying on the sink's append order.
38
+ */
39
+ function queryReconciliationRows(rows, filter = {}) {
40
+ const predicates = buildPredicates(filter);
41
+ const matched = predicates.length === 0
42
+ ? rows
43
+ : rows.filter((row) => predicates.every((predicate) => predicate(row)));
44
+ const sorted = [...matched].sort((a, b) => (0, date_fns_1.compareDesc)((0, date_fns_1.parseISO)(a.ts), (0, date_fns_1.parseISO)(b.ts)));
45
+ const offset = filter.offset ?? 0;
46
+ const limit = filter.limit ?? sorted.length;
47
+ return sorted.slice(offset, offset + limit);
48
+ }
49
+ //# sourceMappingURL=submission-query.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"submission-query.js","sourceRoot":"","sources":["../../../src/lib/telemetry/submission-query.ts"],"names":[],"mappings":";AAAA;;;;;;;GAOG;;;AAEH,uCAAoE;AAuBpE;;;;;GAKG;AACH,SAAS,eAAe,CAAC,MAA+B;IACtD,MAAM,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,YAAY,EAAE,IAAI,EAAE,EAAE,EAAE,GAAG,MAAM,CAAC;IAErE,OAAO;QACL,MAAM,KAAK,SAAS,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,GAAsB,EAAE,EAAE,CAAC,GAAG,CAAC,MAAM,KAAK,MAAM;QAC/E,SAAS,KAAK,SAAS,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,GAAsB,EAAE,EAAE,CAAC,GAAG,CAAC,SAAS,KAAK,SAAS;QACxF,MAAM,KAAK,SAAS,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,GAAsB,EAAE,EAAE,CAAC,GAAG,CAAC,MAAM,KAAK,MAAM;QAC/E,YAAY,KAAK,SAAS;YACxB,CAAC,CAAC,IAAI;YACN,CAAC,CAAC,CAAC,GAAsB,EAAE,EAAE,CAAC,GAAG,CAAC,YAAY,KAAK,YAAY;QACjE,IAAI,KAAK,SAAS;YAChB,CAAC,CAAC,IAAI;YACN,CAAC,CAAC,CAAC,GAAsB,EAAE,EAAE,CAAC,CAAC,IAAA,mBAAQ,EAAC,IAAA,mBAAQ,EAAC,GAAG,CAAC,EAAE,CAAC,EAAE,IAAA,mBAAQ,EAAC,IAAI,CAAC,CAAC;QAC3E,EAAE,KAAK,SAAS,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,GAAsB,EAAE,EAAE,CAAC,CAAC,IAAA,kBAAO,EAAC,IAAA,mBAAQ,EAAC,GAAG,CAAC,EAAE,CAAC,EAAE,IAAA,mBAAQ,EAAC,EAAE,CAAC,CAAC;KAC/F,CAAC,MAAM,CAAC,CAAC,SAAS,EAA2C,EAAE,CAAC,SAAS,KAAK,IAAI,CAAC,CAAC;AACvF,CAAC;AAED;;;;GAIG;AACH,iCACE,IAAyB,EACzB,MAAM,GAA4B,EAAE;IAEpC,MAAM,UAAU,GAAG,eAAe,CAAC,MAAM,CAAC,CAAC;IAC3C,MAAM,OAAO,GACX,UAAU,CAAC,MAAM,KAAK,CAAC;QACrB,CAAC,CAAC,IAAI;QACN,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,UAAU,CAAC,KAAK,CAAC,CAAC,SAAS,EAAE,EAAE,CAAC,SAAS,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC;IAC5E,MAAM,MAAM,GAAG,CAAC,GAAG,OAAO,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,IAAA,sBAAW,EAAC,IAAA,mBAAQ,EAAC,CAAC,CAAC,EAAE,CAAC,EAAE,IAAA,mBAAQ,EAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC;IAExF,MAAM,MAAM,GAAG,MAAM,CAAC,MAAM,IAAI,CAAC,CAAC;IAClC,MAAM,KAAK,GAAG,MAAM,CAAC,KAAK,IAAI,MAAM,CAAC,MAAM,CAAC;IAC5C,OAAO,MAAM,CAAC,KAAK,CAAC,MAAM,EAAE,MAAM,GAAG,KAAK,CAAC,CAAC;AAC9C,CAAC"}
@@ -0,0 +1,52 @@
1
+ /**
2
+ * Read path for the submissions sink: parses `.barnacle/submissions.ndjson`
3
+ * into typed reconciliation records and folds each `beacon` event onto its
4
+ * `submit` record by `requestId`, so one row per run answers both "did we
5
+ * submit" and "did the beacon fire" without a separate database (see
6
+ * feat-002's `reconciliation-record.ts` for the record shapes this reads).
7
+ *
8
+ * The sink is `appendFile` with no locking (`submission-capture.ts`), so a
9
+ * torn final line is expected on a live process — malformed lines are
10
+ * skipped with a warning rather than aborting the whole read.
11
+ */
12
+ import { type BeaconEvent, type ReconciliationRecord, type SubmitRecord } from "../../lib/telemetry/reconciliation-record";
13
+ /**
14
+ * One reconciliation row per run: the submit record's fields plus the
15
+ * outcome of its beacon fire, distinct from submit status so "submitted but
16
+ * the beacon did not fire" is measurable. `beaconStatus` defaults to
17
+ * `"not_fired"` when no matching beacon line ever arrived.
18
+ */
19
+ export interface ReconciliationRow extends Omit<SubmitRecord, "kind"> {
20
+ beaconStatus: BeaconEvent["beaconStatus"] | "not_fired";
21
+ beaconTrackingUrl: string | null;
22
+ beaconTs: string | null;
23
+ beaconDurationMs: number | null;
24
+ }
25
+ /** Options for `readReconciliationRows`. */
26
+ export interface ReadReconciliationRowsOptions {
27
+ /** Override the sink path; used in tests to avoid touching the real file. */
28
+ sinkPath?: string;
29
+ }
30
+ /**
31
+ * Parses NDJSON sink content into typed reconciliation records. Every line
32
+ * shape the sink has ever written (unkinded legacy submit lines, kinded
33
+ * submit lines, beacon lines) is tolerated; a line that is invalid JSON or
34
+ * fails schema validation is skipped with a warning, not fatal to the read.
35
+ */
36
+ export declare function parseReconciliationLines(ndjsonContent: string): ReconciliationRecord[];
37
+ /**
38
+ * Left-joins beacon events onto submit records by `requestId`. Submit rows
39
+ * are the base — a beacon may arrive strictly after its submit line (or
40
+ * never), so an orphan beacon with no matching submit row is dropped rather
41
+ * than synthesizing a phantom row. A later duplicate line (retry) overwrites
42
+ * an earlier one for the same `requestId` and kind.
43
+ */
44
+ export declare function foldReconciliationRecords(records: ReconciliationRecord[]): ReconciliationRow[];
45
+ /**
46
+ * Reads the submissions sink and folds it into one reconciliation row per
47
+ * run, so a plugin can join runs to its own attribution provider's report
48
+ * without re-parsing raw NDJSON. A missing sink file (nothing submitted yet)
49
+ * yields an empty array rather than throwing.
50
+ */
51
+ export declare function readReconciliationRows(opts?: ReadReconciliationRowsOptions): Promise<ReconciliationRow[]>;
52
+ //# sourceMappingURL=submission-reader.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"submission-reader.d.ts","sourceRoot":"","sources":["../../../src/lib/telemetry/submission-reader.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAOH,OAAO,EACL,KAAK,WAAW,EAChB,KAAK,oBAAoB,EAEzB,KAAK,YAAY,EAClB,MAAM,uCAAuC,CAAC;AAI/C;;;;;GAKG;AACH,MAAM,WAAW,iBAAkB,SAAQ,IAAI,CAAC,YAAY,EAAE,MAAM,CAAC;IACnE,YAAY,EAAE,WAAW,CAAC,cAAc,CAAC,GAAG,WAAW,CAAC;IACxD,iBAAiB,EAAE,MAAM,GAAG,IAAI,CAAC;IACjC,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,gBAAgB,EAAE,MAAM,GAAG,IAAI,CAAC;CACjC;AAED,4CAA4C;AAC5C,MAAM,WAAW,6BAA6B;IAC5C,6EAA6E;IAC7E,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAcD;;;;;GAKG;AACH,wBAAgB,wBAAwB,CAAC,aAAa,EAAE,MAAM,GAAG,oBAAoB,EAAE,CA2BtF;AAED;;;;;;GAMG;AACH,wBAAgB,yBAAyB,CAAC,OAAO,EAAE,oBAAoB,EAAE,GAAG,iBAAiB,EAAE,CA6B9F;AAED;;;;;GAKG;AACH,wBAAsB,sBAAsB,CAC1C,IAAI,GAAE,6BAAkC,GACvC,OAAO,CAAC,iBAAiB,EAAE,CAAC,CAM9B"}
@@ -0,0 +1,111 @@
1
+ "use strict";
2
+ /**
3
+ * Read path for the submissions sink: parses `.barnacle/submissions.ndjson`
4
+ * into typed reconciliation records and folds each `beacon` event onto its
5
+ * `submit` record by `requestId`, so one row per run answers both "did we
6
+ * submit" and "did the beacon fire" without a separate database (see
7
+ * feat-002's `reconciliation-record.ts` for the record shapes this reads).
8
+ *
9
+ * The sink is `appendFile` with no locking (`submission-capture.ts`), so a
10
+ * torn final line is expected on a live process — malformed lines are
11
+ * skipped with a warning rather than aborting the whole read.
12
+ */
13
+ Object.defineProperty(exports, "__esModule", { value: true });
14
+ exports.parseReconciliationLines = parseReconciliationLines;
15
+ exports.foldReconciliationRecords = foldReconciliationRecords;
16
+ exports.readReconciliationRows = readReconciliationRows;
17
+ const node_fs_1 = require("node:fs");
18
+ const promises_1 = require("node:fs/promises");
19
+ const config_1 = require("../../config");
20
+ const logging_1 = require("../../lib/logging");
21
+ const reconciliation_record_1 = require("../../lib/telemetry/reconciliation-record");
22
+ const logger = (0, logging_1.getLogger)({ name: "telemetry/submission-reader" });
23
+ /**
24
+ * Parses raw JSON, returning `undefined` on failure instead of throwing, so
25
+ * the line-by-line reader can skip a torn/malformed line without aborting.
26
+ */
27
+ function tryParseJson(line) {
28
+ try {
29
+ return JSON.parse(line);
30
+ }
31
+ catch {
32
+ return undefined;
33
+ }
34
+ }
35
+ /**
36
+ * Parses NDJSON sink content into typed reconciliation records. Every line
37
+ * shape the sink has ever written (unkinded legacy submit lines, kinded
38
+ * submit lines, beacon lines) is tolerated; a line that is invalid JSON or
39
+ * fails schema validation is skipped with a warning, not fatal to the read.
40
+ */
41
+ function parseReconciliationLines(ndjsonContent) {
42
+ const records = [];
43
+ for (const rawLine of ndjsonContent.split("\n")) {
44
+ const line = rawLine.trim();
45
+ if (line.length === 0)
46
+ continue;
47
+ const parsed = tryParseJson(line);
48
+ if (parsed === undefined) {
49
+ logger.warn(`skipping malformed submissions ndjson line (invalid JSON): ${line.slice(0, 200)}`);
50
+ continue;
51
+ }
52
+ const result = reconciliation_record_1.reconciliationRecordSchema.safeParse(parsed);
53
+ if (!result.success) {
54
+ logger.warn(`skipping malformed submissions ndjson line (schema mismatch): ${line.slice(0, 200)}`);
55
+ continue;
56
+ }
57
+ records.push(result.data);
58
+ }
59
+ return records;
60
+ }
61
+ /**
62
+ * Left-joins beacon events onto submit records by `requestId`. Submit rows
63
+ * are the base — a beacon may arrive strictly after its submit line (or
64
+ * never), so an orphan beacon with no matching submit row is dropped rather
65
+ * than synthesizing a phantom row. A later duplicate line (retry) overwrites
66
+ * an earlier one for the same `requestId` and kind.
67
+ */
68
+ function foldReconciliationRecords(records) {
69
+ const rows = new Map();
70
+ for (const record of records) {
71
+ if (record.kind !== "submit")
72
+ continue;
73
+ const { kind: _kind, ...submitFields } = record;
74
+ rows.set(record.requestId, {
75
+ ...submitFields,
76
+ beaconStatus: "not_fired",
77
+ beaconTrackingUrl: null,
78
+ beaconTs: null,
79
+ beaconDurationMs: null,
80
+ });
81
+ }
82
+ for (const record of records) {
83
+ if (record.kind !== "beacon")
84
+ continue;
85
+ const row = rows.get(record.requestId);
86
+ if (row === undefined)
87
+ continue;
88
+ rows.set(record.requestId, {
89
+ ...row,
90
+ beaconStatus: record.beaconStatus,
91
+ beaconTrackingUrl: record.trackingUrl,
92
+ beaconTs: record.ts,
93
+ beaconDurationMs: record.durationMs,
94
+ });
95
+ }
96
+ return Array.from(rows.values());
97
+ }
98
+ /**
99
+ * Reads the submissions sink and folds it into one reconciliation row per
100
+ * run, so a plugin can join runs to its own attribution provider's report
101
+ * without re-parsing raw NDJSON. A missing sink file (nothing submitted yet)
102
+ * yields an empty array rather than throwing.
103
+ */
104
+ async function readReconciliationRows(opts = {}) {
105
+ const sinkPath = opts.sinkPath ?? config_1.config.telemetry.submissionsNdjsonPath;
106
+ if (!(0, node_fs_1.existsSync)(sinkPath))
107
+ return [];
108
+ const content = await (0, promises_1.readFile)(sinkPath, "utf8");
109
+ return foldReconciliationRecords(parseReconciliationLines(content));
110
+ }
111
+ //# sourceMappingURL=submission-reader.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"submission-reader.js","sourceRoot":"","sources":["../../../src/lib/telemetry/submission-reader.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Object-discovery half of the durable S3 read path: enumerates the
3
+ * `<prefix>/submissions/<YYYY-MM-DD>/` keys that could contain rows for a
4
+ * requested `from`/`to` window, so the fetch stage (feat-003) never has to
5
+ * scan the whole telemetry bucket. Entirely inert (no `S3Client`
6
+ * construction, no network calls) when `config.telemetry.s3.bucket` is
7
+ * unset — mirrors `s3-sink.ts`'s inertness contract.
8
+ */
9
+ /** Filter window for `listSubmissionsS3Objects`. Both bounds are ISO datetime strings. */
10
+ export interface ListSubmissionsS3ObjectsOptions {
11
+ from?: string;
12
+ to?: string;
13
+ }
14
+ /**
15
+ * Enumerates the S3 object keys that could contain submissions rows for the
16
+ * requested `from`/`to` window. Returns `[]` without constructing an
17
+ * `S3Client` when no bucket is configured. The result is capped at
18
+ * `config.telemetry.s3.readMaxObjects` — logged with a warning when the cap
19
+ * actually truncated the listing — so a query spanning months of shipped
20
+ * NDJSON can't fan out unbounded S3 calls in the fetch stage that follows.
21
+ */
22
+ export declare function listSubmissionsS3Objects(opts?: ListSubmissionsS3ObjectsOptions): Promise<string[]>;
23
+ //# sourceMappingURL=submissions-s3-objects.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"submissions-s3-objects.d.ts","sourceRoot":"","sources":["../../../src/lib/telemetry/submissions-s3-objects.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AA+BH,0FAA0F;AAC1F,MAAM,WAAW,+BAA+B;IAC9C,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,EAAE,CAAC,EAAE,MAAM,CAAC;CACb;AAkFD;;;;;;;GAOG;AACH,wBAAsB,wBAAwB,CAC5C,IAAI,GAAE,+BAAoC,GACzC,OAAO,CAAC,MAAM,EAAE,CAAC,CAgCnB"}
@@ -0,0 +1,132 @@
1
+ "use strict";
2
+ /**
3
+ * Object-discovery half of the durable S3 read path: enumerates the
4
+ * `<prefix>/submissions/<YYYY-MM-DD>/` keys that could contain rows for a
5
+ * requested `from`/`to` window, so the fetch stage (feat-003) never has to
6
+ * scan the whole telemetry bucket. Entirely inert (no `S3Client`
7
+ * construction, no network calls) when `config.telemetry.s3.bucket` is
8
+ * unset — mirrors `s3-sink.ts`'s inertness contract.
9
+ */
10
+ Object.defineProperty(exports, "__esModule", { value: true });
11
+ exports.listSubmissionsS3Objects = listSubmissionsS3Objects;
12
+ const client_s3_1 = require("@aws-sdk/client-s3");
13
+ const date_fns_1 = require("date-fns");
14
+ const config_1 = require("../../config");
15
+ const logging_1 = require("../../lib/logging");
16
+ const logger = (0, logging_1.getLogger)({ name: "telemetry/submissions-s3-objects" });
17
+ const SUBMISSIONS_DIR = "submissions";
18
+ const MS_PER_DAY = 86_400_000;
19
+ /**
20
+ * Bound on how far back an open-ended window reaches. Matches
21
+ * `TELEMETRY_MAX_RETENTION_MS`'s existing 30-day default so a query that
22
+ * omits `from` can't be asked to enumerate the bucket's entire history.
23
+ */
24
+ const DEFAULT_WINDOW_DAYS = 30;
25
+ let client;
26
+ /** Lazily constructs the S3 client. Never called on the bucket-unset path. */
27
+ function getClient() {
28
+ if (!client) {
29
+ client = new client_s3_1.S3Client({ region: config_1.config.bedrock.region });
30
+ }
31
+ return client;
32
+ }
33
+ /**
34
+ * Resolves the inclusive `from`/`to` `Date` bounds used for day-prefix
35
+ * enumeration. `to` defaults to `now` (submissions can't exist in the
36
+ * future); `from` defaults to `DEFAULT_WINDOW_DAYS` before `to` so an
37
+ * unbounded query is still capped.
38
+ */
39
+ function resolveWindow(opts, now) {
40
+ const to = opts.to ? (0, date_fns_1.parseISO)(opts.to) : now;
41
+ const from = opts.from ? (0, date_fns_1.parseISO)(opts.from) : (0, date_fns_1.subDays)(to, DEFAULT_WINDOW_DAYS);
42
+ return { from, to };
43
+ }
44
+ /** UTC midnight (epoch ms) of the calendar day containing `d`. */
45
+ function utcDayStart(d) {
46
+ return Date.UTC(d.getUTCFullYear(), d.getUTCMonth(), d.getUTCDate());
47
+ }
48
+ /**
49
+ * Builds the day-partition prefixes to scan, widened by one day on each
50
+ * side of the requested window. Day boundaries and prefix dates are computed
51
+ * in UTC (matching `s3-sink.ts`'s `buildObjectKey`) so enumeration is
52
+ * timezone-independent — a reader and writer in different zones agree on
53
+ * partition names. The widening covers runs whose `ts` falls near a day
54
+ * boundary and thus land in an adjacent day's partition; the row-level
55
+ * `from`/`to` predicate in `submission-query.ts` does the exact filtering,
56
+ * so this only needs to not miss a partition.
57
+ */
58
+ function dayPrefixes(from, to, prefix) {
59
+ const start = utcDayStart(from) - MS_PER_DAY;
60
+ const end = utcDayStart(to) + MS_PER_DAY;
61
+ const prefixes = [];
62
+ for (let t = start; t <= end; t += MS_PER_DAY) {
63
+ const day = new Date(t).toISOString().slice(0, 10);
64
+ prefixes.push(`${prefix}/${SUBMISSIONS_DIR}/${day}/`);
65
+ }
66
+ return prefixes;
67
+ }
68
+ /**
69
+ * Lists every object key under one day-partition prefix, following
70
+ * `NextContinuationToken` to exhaustion, stopping early once `budget` keys
71
+ * have been collected. `truncated` distinguishes "budget exhausted mid-list"
72
+ * from "listing finished naturally," so the caller only warns when the cap
73
+ * actually dropped objects.
74
+ */
75
+ async function listPrefixKeys(bucket, prefix, budget) {
76
+ if (budget <= 0)
77
+ return { keys: [], truncated: true };
78
+ const keys = [];
79
+ let continuationToken;
80
+ do {
81
+ const response = await getClient().send(new client_s3_1.ListObjectsV2Command({
82
+ Bucket: bucket,
83
+ Prefix: prefix,
84
+ ContinuationToken: continuationToken,
85
+ }));
86
+ for (const object of response.Contents ?? []) {
87
+ if (object.Key === undefined)
88
+ continue;
89
+ if (keys.length >= budget)
90
+ return { keys, truncated: true };
91
+ keys.push(object.Key);
92
+ }
93
+ continuationToken = response.NextContinuationToken;
94
+ } while (continuationToken !== undefined);
95
+ return { keys, truncated: false };
96
+ }
97
+ /**
98
+ * Enumerates the S3 object keys that could contain submissions rows for the
99
+ * requested `from`/`to` window. Returns `[]` without constructing an
100
+ * `S3Client` when no bucket is configured. The result is capped at
101
+ * `config.telemetry.s3.readMaxObjects` — logged with a warning when the cap
102
+ * actually truncated the listing — so a query spanning months of shipped
103
+ * NDJSON can't fan out unbounded S3 calls in the fetch stage that follows.
104
+ */
105
+ async function listSubmissionsS3Objects(opts = {}) {
106
+ const bucket = config_1.config.telemetry.s3.bucket;
107
+ if (!bucket)
108
+ return [];
109
+ const { from, to } = resolveWindow(opts, new Date());
110
+ const prefixes = dayPrefixes(from, to, config_1.config.telemetry.s3.prefix);
111
+ const maxObjects = config_1.config.telemetry.s3.readMaxObjects;
112
+ const keys = [];
113
+ let truncated = false;
114
+ for (const prefix of prefixes) {
115
+ const budget = maxObjects - keys.length;
116
+ if (budget <= 0) {
117
+ truncated = true;
118
+ break;
119
+ }
120
+ const result = await listPrefixKeys(bucket, prefix, budget);
121
+ keys.push(...result.keys);
122
+ if (result.truncated) {
123
+ truncated = true;
124
+ break;
125
+ }
126
+ }
127
+ if (truncated) {
128
+ logger.warn(`submissions-s3-objects: key listing capped at readMaxObjects=${maxObjects}; some objects in the requested window were not returned`);
129
+ }
130
+ return keys;
131
+ }
132
+ //# sourceMappingURL=submissions-s3-objects.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"submissions-s3-objects.js","sourceRoot":"","sources":["../../../src/lib/telemetry/submissions-s3-objects.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Fetch half of the durable S3 read path: pulls the NDJSON objects named by
3
+ * an already-supplied key list (`submissions-s3-objects.ts`'s
4
+ * `listSubmissionsS3Objects`) with bounded concurrency and parses each body
5
+ * into `ReconciliationRecord[]` via `submission-reader.ts`'s
6
+ * `parseReconciliationLines`, so telemetry that only survives in S3 (ECS
7
+ * containers are ephemeral and per-task) becomes readable. Folding beacon
8
+ * lines onto submit records and merging with the local sink live elsewhere.
9
+ *
10
+ * Mirrors `s3-sink.ts`'s inertness contract: no `S3Client` construction, no
11
+ * network calls, when `config.telemetry.s3.bucket` is unset.
12
+ */
13
+ import type { ReconciliationRecord } from "../../lib/telemetry/reconciliation-record";
14
+ /**
15
+ * Fetches every listed S3 object and parses its NDJSON body into
16
+ * reconciliation records. Objects are fetched through a `p-queue` bounded by
17
+ * `config.telemetry.s3.readConcurrency` — no hand-rolled promise pooling per
18
+ * CLAUDE.md's battle-tested-libraries rule. A single object's `GetObject`
19
+ * failure is logged and skipped rather than aborting the read, matching the
20
+ * swallow-and-log posture the rest of `src/lib/telemetry` uses: a read
21
+ * failure should degrade the answer, never 500 the route. Returns `[]`
22
+ * without constructing an `S3Client` when no bucket is configured.
23
+ */
24
+ export declare function fetchSubmissionsS3Records(keys: string[]): Promise<ReconciliationRecord[]>;
25
+ //# sourceMappingURL=submissions-s3-reader.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"submissions-s3-reader.d.ts","sourceRoot":"","sources":["../../../src/lib/telemetry/submissions-s3-reader.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAQH,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,uCAAuC,CAAC;AAsBlF;;;;;;;;;GASG;AACH,wBAAsB,yBAAyB,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,oBAAoB,EAAE,CAAC,CAmB/F"}
@@ -0,0 +1,63 @@
1
+ "use strict";
2
+ /**
3
+ * Fetch half of the durable S3 read path: pulls the NDJSON objects named by
4
+ * an already-supplied key list (`submissions-s3-objects.ts`'s
5
+ * `listSubmissionsS3Objects`) with bounded concurrency and parses each body
6
+ * into `ReconciliationRecord[]` via `submission-reader.ts`'s
7
+ * `parseReconciliationLines`, so telemetry that only survives in S3 (ECS
8
+ * containers are ephemeral and per-task) becomes readable. Folding beacon
9
+ * lines onto submit records and merging with the local sink live elsewhere.
10
+ *
11
+ * Mirrors `s3-sink.ts`'s inertness contract: no `S3Client` construction, no
12
+ * network calls, when `config.telemetry.s3.bucket` is unset.
13
+ */
14
+ var __importDefault = (this && this.__importDefault) || function (mod) {
15
+ return (mod && mod.__esModule) ? mod : { "default": mod };
16
+ };
17
+ Object.defineProperty(exports, "__esModule", { value: true });
18
+ exports.fetchSubmissionsS3Records = fetchSubmissionsS3Records;
19
+ const client_s3_1 = require("@aws-sdk/client-s3");
20
+ const p_queue_1 = __importDefault(require("p-queue"));
21
+ const config_1 = require("../../config");
22
+ const errors_1 = require("../../lib/errors");
23
+ const logging_1 = require("../../lib/logging");
24
+ const submission_reader_1 = require("../../lib/telemetry/submission-reader");
25
+ const logger = (0, logging_1.getLogger)({ name: "telemetry/submissions-s3-reader" });
26
+ let client;
27
+ /** Lazily constructs the S3 client. Never called on the bucket-unset path. */
28
+ function getClient() {
29
+ if (!client) {
30
+ client = new client_s3_1.S3Client({ region: config_1.config.bedrock.region });
31
+ }
32
+ return client;
33
+ }
34
+ /** Fetches and parses one object's body. Empty body yields no records rather than throwing. */
35
+ async function fetchObjectRecords(bucket, key) {
36
+ const response = await getClient().send(new client_s3_1.GetObjectCommand({ Bucket: bucket, Key: key }));
37
+ const body = await response.Body?.transformToString();
38
+ return body ? (0, submission_reader_1.parseReconciliationLines)(body) : [];
39
+ }
40
+ /**
41
+ * Fetches every listed S3 object and parses its NDJSON body into
42
+ * reconciliation records. Objects are fetched through a `p-queue` bounded by
43
+ * `config.telemetry.s3.readConcurrency` — no hand-rolled promise pooling per
44
+ * CLAUDE.md's battle-tested-libraries rule. A single object's `GetObject`
45
+ * failure is logged and skipped rather than aborting the read, matching the
46
+ * swallow-and-log posture the rest of `src/lib/telemetry` uses: a read
47
+ * failure should degrade the answer, never 500 the route. Returns `[]`
48
+ * without constructing an `S3Client` when no bucket is configured.
49
+ */
50
+ async function fetchSubmissionsS3Records(keys) {
51
+ const bucket = config_1.config.telemetry.s3.bucket;
52
+ if (!bucket)
53
+ return [];
54
+ const queue = new p_queue_1.default({ concurrency: config_1.config.telemetry.s3.readConcurrency });
55
+ const results = await Promise.all(keys.map((key) => queue
56
+ .add(() => fetchObjectRecords(bucket, key))
57
+ .catch((err) => {
58
+ logger.warn(`submissions-s3-reader: fetch of ${key} failed, skipping: ${(0, errors_1.toErrorMessage)(err)}`);
59
+ return undefined;
60
+ })));
61
+ return results.flatMap((records) => records ?? []);
62
+ }
63
+ //# sourceMappingURL=submissions-s3-reader.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"submissions-s3-reader.js","sourceRoot":"","sources":["../../../src/lib/telemetry/submissions-s3-reader.ts"],"names":[],"mappings":";AAAA;;;;;;;;;;;GAWG;;;;;;AAEH,kDAAgE;AAChE,sDAA6B;AAE7B,qCAAkC;AAClC,yCAA8C;AAC9C,2CAA0C;AAE1C,yEAA6E;AAE7E,MAAM,MAAM,GAAG,IAAA,mBAAS,EAAC,EAAE,IAAI,EAAE,iCAAiC,EAAE,CAAC,CAAC;AAEtE,IAAI,MAA4B,CAAC;AAEjC,8EAA8E;AAC9E,SAAS,SAAS;IAChB,IAAI,CAAC,MAAM,EAAE,CAAC;QACZ,MAAM,GAAG,IAAI,oBAAQ,CAAC,EAAE,MAAM,EAAE,eAAM,CAAC,OAAO,CAAC,MAAM,EAAE,CAAC,CAAC;IAC3D,CAAC;IACD,OAAO,MAAM,CAAC;AAChB,CAAC;AAED,+FAA+F;AAC/F,KAAK,UAAU,kBAAkB,CAAC,MAAc,EAAE,GAAW;IAC3D,MAAM,QAAQ,GAAG,MAAM,SAAS,EAAE,CAAC,IAAI,CAAC,IAAI,4BAAgB,CAAC,EAAE,MAAM,EAAE,MAAM,EAAE,GAAG,EAAE,GAAG,EAAE,CAAC,CAAC,CAAC;IAC5F,MAAM,IAAI,GAAG,MAAM,QAAQ,CAAC,IAAI,EAAE,iBAAiB,EAAE,CAAC;IACtD,OAAO,IAAI,CAAC,CAAC,CAAC,IAAA,4CAAwB,EAAC,IAAI,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;AACpD,CAAC;AAED;;;;;;;;;GASG;AACI,KAAK,oCAAoC,IAAc;IAC5D,MAAM,MAAM,GAAG,eAAM,CAAC,SAAS,CAAC,EAAE,CAAC,MAAM,CAAC;IAC1C,IAAI,CAAC,MAAM;QAAE,OAAO,EAAE,CAAC;IAEvB,MAAM,KAAK,GAAG,IAAI,iBAAM,CAAC,EAAE,WAAW,EAAE,eAAM,CAAC,SAAS,CAAC,EAAE,CAAC,eAAe,EAAE,CAAC,CAAC;IAC/E,MAAM,OAAO,GAAG,MAAM,OAAO,CAAC,GAAG,CAC/B,IAAI,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,EAAE,CACf,KAAK;SACF,GAAG,CAAC,GAAG,EAAE,CAAC,kBAAkB,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;SAC1C,KAAK,CAAC,CAAC,GAAG,EAAE,EAAE;QACb,MAAM,CAAC,IAAI,CACT,mCAAmC,GAAG,sBAAsB,IAAA,uBAAc,EAAC,GAAG,CAAC,EAAE,CAClF,CAAC;QACF,OAAO,SAAS,CAAC;IACnB,CAAC,CAAC,CACL,CACF,CAAC;IAEF,OAAO,OAAO,CAAC,OAAO,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,OAAO,IAAI,EAAE,CAAC,CAAC;AACrD,CAAC"}
@@ -9,11 +9,22 @@
9
9
  * No CDP behavioral signals are needed; those are only required when extracting
10
10
  * DataDome tokens for subsequent Node HTTP requests.
11
11
  */
12
+ /**
13
+ * The run's opaque reconciliation join keys, threaded through so a
14
+ * beacon-fire outcome can be correlated back to its submit record. Optional
15
+ * so existing `fireTrackingClick` call sites keep compiling unchanged.
16
+ */
17
+ export interface TrackingClickReconciliationContext {
18
+ requestId: string;
19
+ joinKeys: Record<string, unknown> | null;
20
+ }
12
21
  /**
13
22
  * Launches a background tracking click. Returns immediately — the caller
14
- * does not await the result. Errors never propagate.
23
+ * does not await the result. Errors never propagate. `reconciliation` is
24
+ * optional so a fired/failed beacon outcome can be correlated to the run
25
+ * when the caller has resolved join keys; omit it to keep today's behavior.
15
26
  */
16
- export declare function fireTrackingClick(trackingUrl: string, siteId: string): void;
27
+ export declare function fireTrackingClick(trackingUrl: string, siteId: string, reconciliation?: TrackingClickReconciliationContext): void;
17
28
  /**
18
29
  * Awaits all in-flight tracking clicks, used during graceful shutdown to
19
30
  * prevent Browserbase session leaks on SIGTERM.
@@ -1 +1 @@
1
- {"version":3,"file":"tracking-click.d.ts","sourceRoot":"","sources":["../../src/lib/tracking-click.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AA0DH;;;GAGG;AACH,wBAAgB,iBAAiB,CAAC,WAAW,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,IAAI,CAK3E;AAED;;;GAGG;AACH,wBAAsB,mBAAmB,CAAC,SAAS,SAAS,GAAG,OAAO,CAAC,IAAI,CAAC,CAO3E"}
1
+ {"version":3,"file":"tracking-click.d.ts","sourceRoot":"","sources":["../../src/lib/tracking-click.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAqBH;;;;GAIG;AACH,MAAM,WAAW,kCAAkC;IACjD,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAC;CAC1C;AAiFD;;;;;GAKG;AACH,wBAAgB,iBAAiB,CAC/B,WAAW,EAAE,MAAM,EACnB,MAAM,EAAE,MAAM,EACd,cAAc,CAAC,EAAE,kCAAkC,GAClD,IAAI,CAKN;AAED;;;GAGG;AACH,wBAAsB,mBAAmB,CAAC,SAAS,SAAS,GAAG,OAAO,CAAC,IAAI,CAAC,CAO3E"}
@@ -16,18 +16,34 @@ exports.drainTrackingClicks = drainTrackingClicks;
16
16
  const dd_metrics_1 = require("../lib/dd-metrics");
17
17
  const errors_1 = require("../lib/errors");
18
18
  const logging_1 = require("../lib/logging");
19
+ const beacon_capture_1 = require("../lib/telemetry/beacon-capture");
19
20
  const session_browserbase_1 = require("../scraper/session-browserbase");
20
21
  const logger = (0, logging_1.getLogger)({ name: "tracking-click" });
21
22
  const NAVIGATE_TIMEOUT_MS = 30_000;
22
23
  const SETTLE_WAIT_MS = 5_000;
23
24
  const BROWSERBASE_SESSION_TIMEOUT_SECONDS = 300;
24
25
  const inFlightClicks = new Set();
26
+ /**
27
+ * Wraps `captureBeaconEvent` so a throwing/rejecting capture sink can never
28
+ * propagate into the tracking-click fire-and-forget path — the same defense
29
+ * `emitEnvelopeSafely` applies around `captureSubmissionEnvelope` in
30
+ * loader.ts, since `captureBeaconEvent`'s own internal swallow can be
31
+ * bypassed by a test double or an unexpected synchronous throw.
32
+ */
33
+ async function captureBeaconOutcomeSafely(input) {
34
+ try {
35
+ await (0, beacon_capture_1.captureBeaconEvent)(input);
36
+ }
37
+ catch (err) {
38
+ logger.warn(`beacon outcome capture failed: ${(0, errors_1.toErrorMessage)(err)}`);
39
+ }
40
+ }
25
41
  /**
26
42
  * Navigates a Browserbase session to the tracking URL. Errors are logged
27
43
  * and swallowed — the apply already succeeded, so a failed tracking click
28
44
  * is a monitoring concern, not a runtime failure.
29
45
  */
30
- async function executeTrackingClick(trackingUrl, siteId) {
46
+ async function executeTrackingClick(trackingUrl, siteId, reconciliation) {
31
47
  const startedAt = Date.now();
32
48
  (0, dd_metrics_1.recordTrackingClickAttempt)(siteId);
33
49
  let session;
@@ -41,10 +57,30 @@ async function executeTrackingClick(trackingUrl, siteId) {
41
57
  await page.waitForTimeout(SETTLE_WAIT_MS);
42
58
  (0, dd_metrics_1.recordTrackingClickSuccess)(siteId);
43
59
  logger.info(`tracking click success site=${siteId} url=${trackingUrl.slice(0, 120)} durationMs=${Date.now() - startedAt}`);
60
+ if (reconciliation) {
61
+ await captureBeaconOutcomeSafely({
62
+ requestId: reconciliation.requestId,
63
+ siteId,
64
+ joinKeys: reconciliation.joinKeys,
65
+ beaconStatus: "fired",
66
+ trackingUrl,
67
+ durationMs: Date.now() - startedAt,
68
+ });
69
+ }
44
70
  }
45
71
  catch (err) {
46
72
  (0, dd_metrics_1.recordTrackingClickFailure)(siteId, err instanceof Error ? err.constructor.name : "unknown");
47
73
  logger.warn(`tracking click failed site=${siteId} url=${trackingUrl.slice(0, 120)}: ${(0, errors_1.toErrorMessage)(err)}`);
74
+ if (reconciliation) {
75
+ await captureBeaconOutcomeSafely({
76
+ requestId: reconciliation.requestId,
77
+ siteId,
78
+ joinKeys: reconciliation.joinKeys,
79
+ beaconStatus: "failed",
80
+ trackingUrl,
81
+ durationMs: Date.now() - startedAt,
82
+ });
83
+ }
48
84
  }
49
85
  finally {
50
86
  (0, dd_metrics_1.recordTrackingClickDuration)(siteId, Date.now() - startedAt);
@@ -57,10 +93,12 @@ async function executeTrackingClick(trackingUrl, siteId) {
57
93
  }
58
94
  /**
59
95
  * Launches a background tracking click. Returns immediately — the caller
60
- * does not await the result. Errors never propagate.
96
+ * does not await the result. Errors never propagate. `reconciliation` is
97
+ * optional so a fired/failed beacon outcome can be correlated to the run
98
+ * when the caller has resolved join keys; omit it to keep today's behavior.
61
99
  */
62
- function fireTrackingClick(trackingUrl, siteId) {
63
- const promise = executeTrackingClick(trackingUrl, siteId).finally(() => {
100
+ function fireTrackingClick(trackingUrl, siteId, reconciliation) {
101
+ const promise = executeTrackingClick(trackingUrl, siteId, reconciliation).finally(() => {
64
102
  inFlightClicks.delete(promise);
65
103
  });
66
104
  inFlightClicks.add(promise);
@@ -1 +1 @@
1
- {"version":3,"file":"tracking-click.js","sourceRoot":"","sources":["../../src/lib/tracking-click.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"tracking-click.js","sourceRoot":"","sources":["../../src/lib/tracking-click.ts"],"names":[],"mappings":"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"}