@enricai/barnacle 1.6.9 → 1.6.11
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/README.md +71 -3
- package/dist/api/routes/submissions.d.ts +21 -0
- package/dist/api/routes/submissions.d.ts.map +1 -0
- package/dist/api/routes/submissions.js +48 -0
- package/dist/api/routes/submissions.js.map +1 -0
- package/dist/api/schemas/submissions.d.ts +110 -0
- package/dist/api/schemas/submissions.d.ts.map +1 -0
- package/dist/api/schemas/submissions.js +77 -0
- package/dist/api/schemas/submissions.js.map +1 -0
- package/dist/config.d.ts +34 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +5 -0
- package/dist/config.js.map +1 -1
- package/dist/lib/telemetry/beacon-capture.d.ts +37 -0
- package/dist/lib/telemetry/beacon-capture.d.ts.map +1 -0
- package/dist/lib/telemetry/beacon-capture.js +47 -0
- package/dist/lib/telemetry/beacon-capture.js.map +1 -0
- package/dist/lib/telemetry/reconciliation-record.d.ts +105 -0
- package/dist/lib/telemetry/reconciliation-record.d.ts.map +1 -0
- package/dist/lib/telemetry/reconciliation-record.js +76 -0
- package/dist/lib/telemetry/reconciliation-record.js.map +1 -0
- package/dist/lib/telemetry/reconciliation-source.d.ts +33 -0
- package/dist/lib/telemetry/reconciliation-source.d.ts.map +1 -0
- package/dist/lib/telemetry/reconciliation-source.js +91 -0
- package/dist/lib/telemetry/reconciliation-source.js.map +1 -0
- package/dist/lib/telemetry/s3-sink.d.ts.map +1 -1
- package/dist/lib/telemetry/s3-sink.js +1 -2
- package/dist/lib/telemetry/s3-sink.js.map +1 -1
- package/dist/lib/telemetry/submission-capture.d.ts +30 -9
- package/dist/lib/telemetry/submission-capture.d.ts.map +1 -1
- package/dist/lib/telemetry/submission-capture.js +32 -21
- package/dist/lib/telemetry/submission-capture.js.map +1 -1
- package/dist/lib/telemetry/submission-query.d.ts +32 -0
- package/dist/lib/telemetry/submission-query.d.ts.map +1 -0
- package/dist/lib/telemetry/submission-query.js +49 -0
- package/dist/lib/telemetry/submission-query.js.map +1 -0
- package/dist/lib/telemetry/submission-reader.d.ts +52 -0
- package/dist/lib/telemetry/submission-reader.d.ts.map +1 -0
- package/dist/lib/telemetry/submission-reader.js +111 -0
- package/dist/lib/telemetry/submission-reader.js.map +1 -0
- package/dist/lib/telemetry/submissions-s3-objects.d.ts +23 -0
- package/dist/lib/telemetry/submissions-s3-objects.d.ts.map +1 -0
- package/dist/lib/telemetry/submissions-s3-objects.js +132 -0
- package/dist/lib/telemetry/submissions-s3-objects.js.map +1 -0
- package/dist/lib/telemetry/submissions-s3-reader.d.ts +25 -0
- package/dist/lib/telemetry/submissions-s3-reader.d.ts.map +1 -0
- package/dist/lib/telemetry/submissions-s3-reader.js +63 -0
- package/dist/lib/telemetry/submissions-s3-reader.js.map +1 -0
- package/dist/lib/tracking-click.d.ts +13 -2
- package/dist/lib/tracking-click.d.ts.map +1 -1
- package/dist/lib/tracking-click.js +42 -4
- package/dist/lib/tracking-click.js.map +1 -1
- package/dist/plugins/loader.d.ts +17 -3
- package/dist/plugins/loader.d.ts.map +1 -1
- package/dist/plugins/loader.js +59 -6
- package/dist/plugins/loader.js.map +1 -1
- package/dist/scraper/cookie-jar.d.ts +10 -4
- package/dist/scraper/cookie-jar.d.ts.map +1 -1
- package/dist/scraper/cookie-jar.js +10 -5
- package/dist/scraper/cookie-jar.js.map +1 -1
- package/dist/scraper/deep-locator-candidates.d.ts +84 -0
- package/dist/scraper/deep-locator-candidates.d.ts.map +1 -0
- package/dist/scraper/deep-locator-candidates.js +206 -0
- package/dist/scraper/deep-locator-candidates.js.map +1 -0
- package/dist/scraper/deep-locator-fake.d.ts +116 -0
- package/dist/scraper/deep-locator-fake.d.ts.map +1 -0
- package/dist/scraper/deep-locator-fake.js +128 -0
- package/dist/scraper/deep-locator-fake.js.map +1 -0
- package/dist/scraper/flow-runner.d.ts +28 -1
- package/dist/scraper/flow-runner.d.ts.map +1 -1
- package/dist/scraper/flow-runner.js +204 -14
- package/dist/scraper/flow-runner.js.map +1 -1
- package/dist/scraper/frame-target.d.ts +60 -14
- package/dist/scraper/frame-target.d.ts.map +1 -1
- package/dist/scraper/frame-target.js +126 -38
- package/dist/scraper/frame-target.js.map +1 -1
- package/dist/scraper/stagehand-guard.d.ts +14 -0
- package/dist/scraper/stagehand-guard.d.ts.map +1 -1
- package/dist/scraper/stagehand-guard.js +28 -8
- package/dist/scraper/stagehand-guard.js.map +1 -1
- package/dist/scraper/watchdog.d.ts +38 -0
- package/dist/scraper/watchdog.d.ts.map +1 -0
- package/dist/scraper/watchdog.js +54 -0
- package/dist/scraper/watchdog.js.map +1 -0
- package/dist/scripts/recon-browser.d.ts.map +1 -1
- package/dist/scripts/recon-browser.js +14 -3
- package/dist/scripts/recon-browser.js.map +1 -1
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +2 -0
- package/dist/server.js.map +1 -1
- package/dist/site-plugin.d.ts +12 -0
- package/dist/site-plugin.d.ts.map +1 -1
- 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;
|
|
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":";AAAA;;;;;;;;;;GAUG;;;;AAEH,iDAK0B;AAC1B,yCAA8C;AAC9C,2CAA0C;AAC1C,uEAAgF;AAEhF,MAAM,MAAM,GAAG,IAAA,mBAAS,EAAC,EAAE,IAAI,EAAE,gBAAgB,EAAE,CAAC,CAAC;AAErD,MAAM,mBAAmB,GAAG,MAAM,CAAC;AACnC,MAAM,cAAc,GAAG,KAAK,CAAC;AAC7B,MAAM,mCAAmC,GAAG,GAAG,CAAC;AAEhD,MAAM,cAAc,GAAG,IAAI,GAAG,EAAiB,CAAC;
|
|
1
|
+
{"version":3,"file":"tracking-click.js","sourceRoot":"","sources":["../../src/lib/tracking-click.ts"],"names":[],"mappings":"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"}
|