Setting the file. One moment.
Bi Sink · Rp Telemetry · wix/skills · Skills Docs
ContentsBack to the top of the page
Number 18.3
Position 3 of 4
Type JavaScript
Size 10 KB
Lines 250 lib/ bi-sink.js
JavaScript · 250 lines · 10 KB
// Standard identity fields stay snake_case (`logged_user_id`).
15 // 2. Every GUID-typed field must be a real GUID; a non-hex value fails
16 // validation before routing and drops the whole event.
17 // 3. Every row carries `logged_user_id` = the operator's wix_user_id (GUID).
18 //
19 // frog's HTTP status is not an ingestion receipt — only a Trino read-back proves
20 // arrival. The recorder treats any non-2xx or network error as a push failure
21 // (journaled into telemetry_health); a 2xx merely means "handed to frog".
22 //
23 // Env switches (tests / offline dev only — production runs always emit):
24 // RP_TELEMETRY_BI_ENDPOINT override the frog endpoint (contract-test mock)
25 // RP_TELEMETRY_BI_DISABLED=1 skip all pushes entirely
26
27 const SRC = 10 ;
28 const EVID_RUN = 5012 ; // replatform_run — run-grain lifecycle, phase started|finalized
29 const EVID_RUN_EVENT = 5013 ; // replatform_run_event — one row per observation class
30 const DEFAULT_ENDPOINT = 'https://frog.wix.com/migration-data-validator' ;
31 const MAX_EVENTS_PER_REQUEST = 100 ;
32 const MAX_BODY_BYTES = 240 * 1024 ;
33 const PUSH_TIMEOUT_MS = 8000 ;
34
35 const GUID_RE = / ^ [0-9a-f] {8} - [0-9a-f] {4} - [0-9a-f] {4} - [0-9a-f] {4} - [0-9a-f] {12}$ / i ;
36
37 function isGuid ( value ) {
38 return typeof value === 'string' && GUID_RE . test (value);
39 }
40
41 function disabled () {
42 return process.env. RP_TELEMETRY_BI_DISABLED === '1' ;
43 }
44
45 function endpoint () {
46 return process.env. RP_TELEMETRY_BI_ENDPOINT || DEFAULT_ENDPOINT ;
47 }
48
49 // Empty-value convention: unknown fields are omitted, never sent as null/"".
50 // JSON-typed schema fields (arrays/objects) are JSON-serialized into their value.
51 // GUID-typed fields are dropped unless they hold a real GUID (hard rule 2 —
52 // sending a non-GUID would silently drop the whole event, not just the field).
53 function put ( fields , key , value , { json = false , guid = false } = {}) {
54 if (value === undefined || value === null ) return ;
55 if (guid && ! isGuid (value)) return ;
56 fields[key] = json ? JSON . stringify (value) : value;
57 }
58
59 // `replatform_run` phase:started — built from the journal's run_start record, so
60 // its content is a pure function of run start and re-pushes are byte-identical
61 // (dedup key: (run_id, phase), latest row wins). `wixUserId` is the one late
62 // addition: identity may arrive via a later dims call, and the row is only
63 // sendable once it has (rule 3), so it rides along when known.
64 function startedRunRow ( runStart , wixUserId ) {
65 const dims = runStart.dims || {};
66 const f = { phase: 'started' };
67 put (f, 'runId' , runStart.run_id, { guid: true });
68 put (f, 'projectId' , runStart.project_id);
69 put (f, 'attempt' , runStart.attempt);
70 put (f, 'sessionCount' , 1 );
71 put (f, 'schemaVersions' , runStart.schema_version);
72 put (f, 'skillsVersion' , runStart.skills_version);
73 put (f, 'runtimeEnv' , runStart.runtime_env, { json: true });
74 put (f, 'wixUserId' , wixUserId, { guid: true });
75 put (f, 'sourcePlatform' , dims.source_platform);
76 put (f, 'sourcePlatformVersion' , dims.source_platform_version);
77 put (f, 'sourceExtensions' , dims.source_extensions, { json: true });
78 put (f, 'sourceSiteUrl' , dims.source_site_url);
79 put (f, 'sourceAcquisition' , dims.source_acquisition);
80 put (f, 'deliveryMode' , dims.delivery_mode);
81 put (f, 'destinationStrategy' , dims.destination_strategy);
82 put (f, 'runStarted' , runStart.ts);
83 return { evid: EVID_RUN , fields: f };
84 }
85
86 // `replatform_run` phase:finalized — the complete rollup. Local names map to the
87 // as-registered BI names here and nowhere else (spec 0007, Registration notes):
88 // schema_version→schemaVersions, verification→verificationType. skills_commit is
89 // local-only (not a registered BI column) and deliberately not sent.
90 function finalizedRunRow ( rollup ) {
91 const f = { phase: 'finalized' };
92 put (f, 'runId' , rollup.run_id, { guid: true });
93 put (f, 'projectId' , rollup.project_id);
94 put (f, 'attempt' , rollup.attempt);
95 put (f, 'sessionCount' , rollup.session_count);
96 put (f, 'schemaVersions' , rollup.schema_version);
97 put (f, 'skillsVersion' , rollup.skills_version);
98 put (f, 'runtimeEnv' , rollup.runtime_env, { json: true });
99 put (f, 'wixUserId' , rollup.wix_user_id, { guid: true });
100 put (f, 'siteId' , rollup.site_id, { guid: true });
101 put (f, 'sourcePlatform' , rollup.source_platform);
102 put (f, 'sourcePlatformVersion' , rollup.source_platform_version);
103 put (f, 'sourceExtensions' , rollup.source_extensions, { json: true });
104 put (f, 'sourceSiteUrl' , rollup.source_site_url);
105 put (f, 'sourceAcquisition' , rollup.source_acquisition);
106 put (f, 'deliveryMode' , rollup.delivery_mode);
107 put (f, 'destinationStrategy' , rollup.destination_strategy);
108 put (f, 'terminalState' , rollup.terminal_state);
109 put (f, 'stoppedAtStage' , rollup.stopped_at_stage);
110 put (f, 'stages' , rollup.stages, { json: true });
111 put (f, 'volumes' , rollup.volumes, { json: true });
112 put (f, 'verificationType' , rollup.verification, { json: true });
113 put (f, 'operatorAcceptance' , rollup.operator_acceptance);
114 put (f, 'telemetryHealth' , rollup.telemetry_health, { json: true });
115 put (f, 'eventCountByType' , rollup.event_count_by_type, { json: true });
116 put (f, 'activeMs' , rollup.active_ms);
117 put (f, 'waitingMs' , rollup.waiting_ms);
118 put (f, 'runStarted' , rollup.run_started);
119 put (f, 'runEnded' , rollup.run_ended);
120 put (f, 'bundleManifest' , rollup.bundle_manifest, { json: true });
121 return { evid: EVID_RUN , fields: f };
122 }
123
124 // `replatform_run_event` — one row per folded observation class. Re-pushing a
125 // class after more occurrences folded in is by design: the dedup key is
126 // (run_id, seq) and the latest row carries the cumulative count. Renames:
127 // event_type→eventTypeName, error_code→errorType, expected/actual→
128 // expectedSkill/actualSkill.
129 function eventRow ( folded ) {
130 const f = {};
131 put (f, 'runId' , folded.run_id, { guid: true });
132 put (f, 'seq' , folded.seq);
133 put (f, 'count' , folded.count);
134 put (f, 'stage' , folded.stage);
135 put (f, 'skill' , folded.skill);
136 put (f, 'eventTypeName' , folded.event_type);
137 put (f, 'subtype' , folded.subtype);
138 put (f, 'entityType' , folded.entity_type);
139 put (f, 'severity' , folded.severity);
140 put (f, 'wixApiSurface' , folded.wix_api_surface);
141 put (f, 'sourceApiSurface' , folded.source_api_surface);
142 put (f, 'wixAppId' , folded.wix_app_id, { guid: true });
143 put (f, 'errorType' , folded.error_code);
144 put (f, 'decisionPoint' , folded.decision_point);
145 put (f, 'retryCount' , folded.retry_count);
146 put (f, 'recovered' , folded.recovered);
147 put (f, 'whatHappened' , folded.what_happened);
148 put (f, 'expectedSkill' , folded.expected);
149 put (f, 'actualSkill' , folded.actual);
150 put (f, 'observedShapes' , folded.observed_shapes, { json: true });
151 put (f, 'shapesSeen' , folded.shapes_seen);
152 put (f, 'evidenceRefs' , folded.evidence_refs, { json: true });
153 return { evid: EVID_RUN_EVENT , fields: f };
154 }
155
156 function envelopeBody ( evid , events ) {
157 return JSON . stringify ({ dt: 0 , g: { src: SRC , evid }, e: events });
158 }
159
160 // A single event over the body cap sheds its unbounded-ish JSON fields before
161 // being declared unsendable. Truncation is surfaced (telemetry_health flag),
162 // never silent.
163 const SHEDDABLE_FIELDS = [ 'observedShapes' , 'evidenceRefs' , 'bundleManifest' , 'stages' , 'volumes' ];
164
165 function shedOversize ( event ) {
166 const f = { ... event.f };
167 let shed = false ;
168 for ( const key of SHEDDABLE_FIELDS ) {
169 if (f[key] === undefined ) continue ;
170 delete f[key];
171 shed = true ;
172 }
173 return shed ? { ... event, f } : null ;
174 }
175
176 async function sendBatch ( evid , events ) {
177 const body = envelopeBody (evid, events);
178 try {
179 const res = await fetch ( endpoint (), {
180 method: 'POST' ,
181 headers: { 'content-type' : 'application/json' },
182 body,
183 signal: AbortSignal. timeout ( PUSH_TIMEOUT_MS ),
184 });
185 if ( ! res.ok) return `http_${ res . status }` ;
186 return null ;
187 } catch (error) {
188 return error && error.name === 'TimeoutError' ? 'timeout' : 'network_error' ;
189 }
190 }
191
192 // Push rows (from the builders above) as the given operator. Never throws:
193 // returns { sent, failures: [{evid, rows, detail}], truncated } and the caller
194 // journals the failures — telemetry must never block a migration.
195 async function pushRows ( rows , loggedUserId ) {
196 const out = { sent: 0 , failures: [], truncated: 0 };
197 if ( disabled () || rows. length === 0 ) return out;
198 if ( ! isGuid (loggedUserId)) {
199 out.failures. push ({ evid: null , rows: rows. length , detail: 'no_operator_identity' });
200 return out;
201 }
202 const byEvid = new Map ();
203 for ( const row of rows) {
204 if ( ! byEvid. has (row.evid)) byEvid. set (row.evid, []);
205 byEvid. get (row.evid). push ({ dt: 0 , f: { ... row.fields, logged_user_id: loggedUserId } });
206 }
207 for ( const [ evid , events ] of byEvid) {
208 const queue = [events];
209 while (queue. length > 0 ) {
210 let chunk = queue. shift ();
211 if (chunk. length > MAX_EVENTS_PER_REQUEST ) {
212 const mid = Math. ceil (chunk. length / 2 );
213 queue. unshift (chunk. slice ( 0 , mid), chunk. slice (mid));
214 continue ;
215 }
216 if (Buffer. byteLength ( envelopeBody (evid, chunk)) > MAX_BODY_BYTES ) {
217 if (chunk. length > 1 ) {
218 const mid = Math. ceil (chunk. length / 2 );
219 queue. unshift (chunk. slice ( 0 , mid), chunk. slice (mid));
220 continue ;
221 }
222 const slimmed = shedOversize (chunk[ 0 ]);
223 if ( ! slimmed || Buffer. byteLength ( envelopeBody (evid, [slimmed])) > MAX_BODY_BYTES ) {
224 out.failures. push ({ evid, rows: 1 , detail: 'oversized_event' });
225 continue ;
226 }
227 out.truncated += 1 ;
228 chunk = [slimmed];
229 }
230 const failureDetail = await sendBatch (evid, chunk);
231 if (failureDetail) out.failures. push ({ evid, rows: chunk. length , detail: failureDetail });
232 else out.sent += chunk. length ;
233 }
234 }
235 return out;
236 }
237
238 module . exports = {
239 SRC,
240 EVID_RUN,
241 EVID_RUN_EVENT,
242 MAX_EVENTS_PER_REQUEST,
243 MAX_BODY_BYTES,
244 isGuid,
245 disabled,
246 startedRunRow,
247 finalizedRunRow,
248 eventRow,
249 pushRows,
250 };