@editframe/elements 0.20.4-beta.0 → 0.21.0-beta.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/DelayedLoadingState.js +0 -27
- package/dist/EF_FRAMEGEN.d.ts +5 -3
- package/dist/EF_FRAMEGEN.js +50 -11
- package/dist/_virtual/_@oxc-project_runtime@0.93.0/helpers/decorate.js +7 -0
- package/dist/elements/ContextProxiesController.js +2 -22
- package/dist/elements/EFAudio.js +4 -8
- package/dist/elements/EFCaptions.js +59 -84
- package/dist/elements/EFImage.js +5 -6
- package/dist/elements/EFMedia/AssetIdMediaEngine.js +2 -4
- package/dist/elements/EFMedia/AssetMediaEngine.js +35 -30
- package/dist/elements/EFMedia/BaseMediaEngine.js +57 -73
- package/dist/elements/EFMedia/BufferedSeekingInput.js +134 -76
- package/dist/elements/EFMedia/JitMediaEngine.js +9 -19
- package/dist/elements/EFMedia/audioTasks/makeAudioBufferTask.js +3 -6
- package/dist/elements/EFMedia/audioTasks/makeAudioFrequencyAnalysisTask.js +1 -1
- package/dist/elements/EFMedia/audioTasks/makeAudioInitSegmentFetchTask.js +1 -1
- package/dist/elements/EFMedia/audioTasks/makeAudioInputTask.js +6 -5
- package/dist/elements/EFMedia/audioTasks/makeAudioSeekTask.js +1 -3
- package/dist/elements/EFMedia/audioTasks/makeAudioSegmentFetchTask.js +1 -1
- package/dist/elements/EFMedia/audioTasks/makeAudioSegmentIdTask.js +1 -1
- package/dist/elements/EFMedia/audioTasks/makeAudioTimeDomainAnalysisTask.js +1 -1
- package/dist/elements/EFMedia/shared/AudioSpanUtils.js +4 -16
- package/dist/elements/EFMedia/shared/BufferUtils.js +2 -15
- package/dist/elements/EFMedia/shared/GlobalInputCache.js +0 -24
- package/dist/elements/EFMedia/shared/PrecisionUtils.js +0 -21
- package/dist/elements/EFMedia/shared/ThumbnailExtractor.js +0 -17
- package/dist/elements/EFMedia/tasks/makeMediaEngineTask.js +1 -10
- package/dist/elements/EFMedia/videoTasks/MainVideoInputCache.d.ts +29 -0
- package/dist/elements/EFMedia/videoTasks/MainVideoInputCache.js +32 -0
- package/dist/elements/EFMedia/videoTasks/ScrubInputCache.js +1 -15
- package/dist/elements/EFMedia/videoTasks/makeScrubVideoBufferTask.js +1 -7
- package/dist/elements/EFMedia/videoTasks/makeScrubVideoInputTask.js +8 -5
- package/dist/elements/EFMedia/videoTasks/makeScrubVideoSeekTask.js +12 -13
- package/dist/elements/EFMedia/videoTasks/makeScrubVideoSegmentIdTask.js +1 -1
- package/dist/elements/EFMedia/videoTasks/makeUnifiedVideoSeekTask.js +134 -70
- package/dist/elements/EFMedia/videoTasks/makeVideoBufferTask.js +7 -11
- package/dist/elements/EFMedia.js +26 -24
- package/dist/elements/EFSourceMixin.js +5 -7
- package/dist/elements/EFSurface.js +6 -9
- package/dist/elements/EFTemporal.js +19 -37
- package/dist/elements/EFThumbnailStrip.js +16 -59
- package/dist/elements/EFTimegroup.js +95 -90
- package/dist/elements/EFVideo.d.ts +6 -2
- package/dist/elements/EFVideo.js +142 -107
- package/dist/elements/EFWaveform.js +18 -27
- package/dist/elements/SampleBuffer.js +2 -5
- package/dist/elements/TargetController.js +3 -3
- package/dist/elements/durationConverter.js +4 -4
- package/dist/elements/updateAnimations.js +14 -35
- package/dist/gui/ContextMixin.js +23 -52
- package/dist/gui/EFConfiguration.js +7 -7
- package/dist/gui/EFControls.js +5 -5
- package/dist/gui/EFFilmstrip.js +77 -98
- package/dist/gui/EFFitScale.js +5 -6
- package/dist/gui/EFFocusOverlay.js +4 -4
- package/dist/gui/EFPreview.js +4 -4
- package/dist/gui/EFScrubber.js +9 -9
- package/dist/gui/EFTimeDisplay.js +5 -5
- package/dist/gui/EFToggleLoop.js +4 -4
- package/dist/gui/EFTogglePlay.js +5 -5
- package/dist/gui/EFWorkbench.js +5 -5
- package/dist/gui/TWMixin2.js +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/otel/BridgeSpanExporter.d.ts +13 -0
- package/dist/otel/BridgeSpanExporter.js +87 -0
- package/dist/otel/setupBrowserTracing.d.ts +12 -0
- package/dist/otel/setupBrowserTracing.js +30 -0
- package/dist/otel/tracingHelpers.d.ts +34 -0
- package/dist/otel/tracingHelpers.js +113 -0
- package/dist/transcoding/cache/RequestDeduplicator.js +0 -21
- package/dist/transcoding/cache/URLTokenDeduplicator.js +1 -21
- package/dist/transcoding/utils/UrlGenerator.js +2 -19
- package/dist/utils/LRUCache.js +6 -53
- package/package.json +10 -2
- package/src/elements/EFCaptions.browsertest.ts +2 -0
- package/src/elements/EFMedia/AssetMediaEngine.ts +65 -37
- package/src/elements/EFMedia/BaseMediaEngine.ts +110 -52
- package/src/elements/EFMedia/BufferedSeekingInput.ts +218 -101
- package/src/elements/EFMedia/audioTasks/makeAudioInputTask.ts +7 -3
- package/src/elements/EFMedia/videoTasks/MainVideoInputCache.ts +76 -0
- package/src/elements/EFMedia/videoTasks/makeScrubVideoInputTask.ts +16 -10
- package/src/elements/EFMedia/videoTasks/makeScrubVideoSeekTask.ts +7 -1
- package/src/elements/EFMedia/videoTasks/makeUnifiedVideoSeekTask.ts +222 -116
- package/src/elements/EFMedia.ts +16 -1
- package/src/elements/EFTimegroup.browsertest.ts +10 -8
- package/src/elements/EFTimegroup.ts +164 -76
- package/src/elements/EFVideo.browsertest.ts +19 -27
- package/src/elements/EFVideo.ts +203 -101
- package/src/otel/BridgeSpanExporter.ts +150 -0
- package/src/otel/setupBrowserTracing.ts +68 -0
- package/src/otel/tracingHelpers.ts +251 -0
- package/types.json +1 -1
package/src/elements/EFVideo.ts
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import { Task } from "@lit/task";
|
|
2
|
+
import { context, trace } from "@opentelemetry/api";
|
|
2
3
|
import debug from "debug";
|
|
3
4
|
import { css, html, type PropertyValueMap } from "lit";
|
|
4
5
|
import { customElement, property, state } from "lit/decorators.js";
|
|
5
6
|
import { createRef, ref } from "lit/directives/ref.js";
|
|
6
|
-
|
|
7
7
|
import { DelayedLoadingState } from "../DelayedLoadingState.js";
|
|
8
8
|
import { TWMixin } from "../gui/TWMixin.js";
|
|
9
|
+
import { withSpan, withSpanSync } from "../otel/tracingHelpers.js";
|
|
9
10
|
import { makeScrubVideoBufferTask } from "./EFMedia/videoTasks/makeScrubVideoBufferTask.ts";
|
|
10
11
|
import { makeScrubVideoInitSegmentFetchTask } from "./EFMedia/videoTasks/makeScrubVideoInitSegmentFetchTask.ts";
|
|
11
12
|
import { makeScrubVideoInputTask } from "./EFMedia/videoTasks/makeScrubVideoInputTask.ts";
|
|
@@ -203,11 +204,40 @@ export class EFVideo extends TWMixin(EFMedia) {
|
|
|
203
204
|
console.error("frameTask error", error);
|
|
204
205
|
},
|
|
205
206
|
onComplete: () => {},
|
|
206
|
-
task: async ([_desiredSeekTimeMs]) => {
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
207
|
+
task: async ([_desiredSeekTimeMs], { signal }) => {
|
|
208
|
+
const t0 = performance.now();
|
|
209
|
+
|
|
210
|
+
await withSpan(
|
|
211
|
+
"video.frameTask",
|
|
212
|
+
{
|
|
213
|
+
elementId: this.id || "unknown",
|
|
214
|
+
desiredSeekTimeMs: _desiredSeekTimeMs,
|
|
215
|
+
src: this.src || "none",
|
|
216
|
+
},
|
|
217
|
+
undefined,
|
|
218
|
+
async (span) => {
|
|
219
|
+
const t1 = performance.now();
|
|
220
|
+
span.setAttribute("preworkMs", t1 - t0);
|
|
221
|
+
|
|
222
|
+
this.unifiedVideoSeekTask.run();
|
|
223
|
+
const t2 = performance.now();
|
|
224
|
+
span.setAttribute("seekRunMs", t2 - t1);
|
|
225
|
+
|
|
226
|
+
await this.unifiedVideoSeekTask.taskComplete;
|
|
227
|
+
const t3 = performance.now();
|
|
228
|
+
span.setAttribute("seekAwaitMs", t3 - t2);
|
|
229
|
+
if (signal.aborted) {
|
|
230
|
+
span.setAttribute("aborted", true);
|
|
231
|
+
return;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
const t4 = performance.now();
|
|
235
|
+
this.paint(_desiredSeekTimeMs, span);
|
|
236
|
+
const t5 = performance.now();
|
|
237
|
+
span.setAttribute("paintMs", t5 - t4);
|
|
238
|
+
span.setAttribute("totalFrameMs", t5 - t0);
|
|
239
|
+
},
|
|
240
|
+
);
|
|
211
241
|
},
|
|
212
242
|
});
|
|
213
243
|
|
|
@@ -244,64 +274,92 @@ export class EFVideo extends TWMixin(EFMedia) {
|
|
|
244
274
|
};
|
|
245
275
|
}
|
|
246
276
|
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
277
|
+
/**
|
|
278
|
+
* Paint the current video frame to canvas
|
|
279
|
+
* Called by frameTask after seek is complete
|
|
280
|
+
*/
|
|
281
|
+
paint(seekToMs: number, parentSpan?: any): void {
|
|
282
|
+
const parentContext = parentSpan
|
|
283
|
+
? trace.setSpan(context.active(), parentSpan)
|
|
284
|
+
: undefined;
|
|
285
|
+
|
|
286
|
+
withSpanSync(
|
|
287
|
+
"video.paint",
|
|
288
|
+
{
|
|
289
|
+
elementId: this.id || "unknown",
|
|
290
|
+
seekToMs,
|
|
291
|
+
src: this.src || "none",
|
|
292
|
+
},
|
|
293
|
+
parentContext,
|
|
294
|
+
(span) => {
|
|
295
|
+
const t0 = performance.now();
|
|
296
|
+
|
|
297
|
+
// Check if we're in production rendering mode vs preview mode
|
|
298
|
+
const isProductionRendering = this.isInProductionRenderingMode();
|
|
299
|
+
const t1 = performance.now();
|
|
300
|
+
span.setAttribute("isProductionRendering", isProductionRendering);
|
|
301
|
+
span.setAttribute("modeCheckMs", t1 - t0);
|
|
302
|
+
|
|
303
|
+
// Unified video system: smart routing to scrub or main, with background upgrades
|
|
304
|
+
// Note: frameTask guarantees unifiedVideoSeekTask is complete before calling paint
|
|
305
|
+
try {
|
|
306
|
+
const t2 = performance.now();
|
|
307
|
+
const videoSample = this.unifiedVideoSeekTask.value;
|
|
308
|
+
span.setAttribute("hasVideoSample", !!videoSample);
|
|
309
|
+
span.setAttribute("valueAccessMs", t2 - t1);
|
|
310
|
+
|
|
311
|
+
if (videoSample) {
|
|
312
|
+
const t3 = performance.now();
|
|
313
|
+
const videoFrame = videoSample.toVideoFrame();
|
|
314
|
+
const t4 = performance.now();
|
|
315
|
+
span.setAttribute("toVideoFrameMs", t4 - t3);
|
|
316
|
+
|
|
317
|
+
try {
|
|
318
|
+
const t5 = performance.now();
|
|
319
|
+
this.displayFrame(videoFrame, seekToMs, span);
|
|
320
|
+
const t6 = performance.now();
|
|
321
|
+
span.setAttribute("displayFrameMs", t6 - t5);
|
|
322
|
+
} finally {
|
|
323
|
+
videoFrame.close();
|
|
324
|
+
}
|
|
269
325
|
}
|
|
326
|
+
} catch (error) {
|
|
327
|
+
console.warn("Unified video pipeline error:", error);
|
|
270
328
|
}
|
|
271
|
-
} catch (error) {
|
|
272
|
-
console.warn("Unified video pipeline error:", error);
|
|
273
|
-
}
|
|
274
329
|
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
330
|
+
// EF_FRAMEGEN-aware rendering mode detection
|
|
331
|
+
if (!isProductionRendering) {
|
|
332
|
+
// Preview mode: skip rendering during initialization to prevent artifacts
|
|
333
|
+
if (
|
|
334
|
+
!this.rootTimegroup ||
|
|
335
|
+
(this.rootTimegroup.currentTimeMs === 0 &&
|
|
336
|
+
this.desiredSeekTimeMs === 0)
|
|
337
|
+
) {
|
|
338
|
+
span.setAttribute("skipped", "preview-initialization");
|
|
339
|
+
return; // Skip initialization frame in preview mode
|
|
340
|
+
}
|
|
341
|
+
// Preview mode: proceed with rendering
|
|
342
|
+
} else {
|
|
343
|
+
// Production rendering mode: only render when EF_FRAMEGEN has explicitly started frame rendering
|
|
344
|
+
// This prevents initialization frames before the actual render sequence begins
|
|
345
|
+
if (!this.rootTimegroup) {
|
|
346
|
+
span.setAttribute("skipped", "no-root-timegroup");
|
|
347
|
+
return;
|
|
348
|
+
}
|
|
292
349
|
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
350
|
+
if (!this.isFrameRenderingActive()) {
|
|
351
|
+
span.setAttribute("skipped", "frame-rendering-not-active");
|
|
352
|
+
return; // Wait for EF_FRAMEGEN to start frame sequence
|
|
353
|
+
}
|
|
296
354
|
|
|
297
|
-
|
|
298
|
-
|
|
355
|
+
// Production mode: EF_FRAMEGEN has started frame sequence, proceed with rendering
|
|
356
|
+
}
|
|
299
357
|
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
}
|
|
303
|
-
|
|
304
|
-
}
|
|
358
|
+
const tEnd = performance.now();
|
|
359
|
+
span.setAttribute("totalPaintMs", tEnd - t0);
|
|
360
|
+
},
|
|
361
|
+
);
|
|
362
|
+
}
|
|
305
363
|
|
|
306
364
|
/**
|
|
307
365
|
* Clear the canvas when element becomes inactive
|
|
@@ -318,54 +376,98 @@ export class EFVideo extends TWMixin(EFMedia) {
|
|
|
318
376
|
/**
|
|
319
377
|
* Display a video frame on the canvas
|
|
320
378
|
*/
|
|
321
|
-
displayFrame(frame: VideoFrame, seekToMs: number):
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
379
|
+
displayFrame(frame: VideoFrame, seekToMs: number, parentSpan?: any): void {
|
|
380
|
+
const parentContext = parentSpan
|
|
381
|
+
? trace.setSpan(context.active(), parentSpan)
|
|
382
|
+
: undefined;
|
|
383
|
+
|
|
384
|
+
withSpanSync(
|
|
385
|
+
"video.displayFrame",
|
|
386
|
+
{
|
|
387
|
+
elementId: this.id || "unknown",
|
|
388
|
+
seekToMs,
|
|
389
|
+
format: frame.format || "unknown",
|
|
390
|
+
width: frame.codedWidth,
|
|
391
|
+
height: frame.codedHeight,
|
|
392
|
+
},
|
|
393
|
+
parentContext,
|
|
394
|
+
(span) => {
|
|
395
|
+
const t0 = performance.now();
|
|
337
396
|
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
this.canvasElement.height !== frame.codedHeight
|
|
342
|
-
) {
|
|
343
|
-
log("trace: updating canvas dimensions", {
|
|
344
|
-
width: frame.codedWidth,
|
|
345
|
-
height: frame.codedHeight,
|
|
397
|
+
log("trace: displayFrame start", {
|
|
398
|
+
seekToMs,
|
|
399
|
+
frameFormat: frame.format,
|
|
346
400
|
});
|
|
347
|
-
this.canvasElement.width = frame.codedWidth;
|
|
348
|
-
this.canvasElement.height = frame.codedHeight;
|
|
349
|
-
}
|
|
350
|
-
}
|
|
351
401
|
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
402
|
+
if (!this.canvasElement) {
|
|
403
|
+
log("trace: displayFrame aborted - no canvas element");
|
|
404
|
+
throw new Error(
|
|
405
|
+
`Frame display failed: Canvas element is not available at time ${seekToMs}ms. The video component may not be properly initialized.`,
|
|
406
|
+
);
|
|
407
|
+
}
|
|
408
|
+
const t1 = performance.now();
|
|
409
|
+
span.setAttribute("getCanvasMs", Math.round((t1 - t0) * 100) / 100);
|
|
410
|
+
|
|
411
|
+
const ctx = this.canvasElement.getContext("2d");
|
|
412
|
+
const t2 = performance.now();
|
|
413
|
+
span.setAttribute("getCtxMs", Math.round((t2 - t1) * 100) / 100);
|
|
414
|
+
|
|
415
|
+
if (!ctx) {
|
|
416
|
+
log("trace: displayFrame aborted - no canvas context");
|
|
417
|
+
throw new Error(
|
|
418
|
+
`Frame display failed: Unable to get 2D canvas context at time ${seekToMs}ms. This may indicate a browser compatibility issue or canvas corruption.`,
|
|
419
|
+
);
|
|
420
|
+
}
|
|
358
421
|
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
422
|
+
let resized = false;
|
|
423
|
+
if (frame?.codedWidth && frame?.codedHeight) {
|
|
424
|
+
if (
|
|
425
|
+
this.canvasElement.width !== frame.codedWidth ||
|
|
426
|
+
this.canvasElement.height !== frame.codedHeight
|
|
427
|
+
) {
|
|
428
|
+
log("trace: updating canvas dimensions", {
|
|
429
|
+
width: frame.codedWidth,
|
|
430
|
+
height: frame.codedHeight,
|
|
431
|
+
});
|
|
432
|
+
this.canvasElement.width = frame.codedWidth;
|
|
433
|
+
this.canvasElement.height = frame.codedHeight;
|
|
434
|
+
resized = true;
|
|
435
|
+
const t3 = performance.now();
|
|
436
|
+
span.setAttribute("resizeMs", Math.round((t3 - t2) * 100) / 100);
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
span.setAttribute("canvasResized", resized);
|
|
440
|
+
|
|
441
|
+
if (frame.format === null) {
|
|
442
|
+
log("trace: displayFrame aborted - null frame format");
|
|
443
|
+
throw new Error(
|
|
444
|
+
`Frame display failed: Video frame has null format at time ${seekToMs}ms. This indicates corrupted or incompatible video data.`,
|
|
445
|
+
);
|
|
446
|
+
}
|
|
367
447
|
|
|
368
|
-
|
|
448
|
+
const tDrawStart = performance.now();
|
|
449
|
+
ctx.drawImage(
|
|
450
|
+
frame,
|
|
451
|
+
0,
|
|
452
|
+
0,
|
|
453
|
+
this.canvasElement.width,
|
|
454
|
+
this.canvasElement.height,
|
|
455
|
+
);
|
|
456
|
+
const tDrawEnd = performance.now();
|
|
457
|
+
span.setAttribute(
|
|
458
|
+
"drawImageMs",
|
|
459
|
+
Math.round((tDrawEnd - tDrawStart) * 100) / 100,
|
|
460
|
+
);
|
|
461
|
+
span.setAttribute(
|
|
462
|
+
"totalDisplayMs",
|
|
463
|
+
Math.round((tDrawEnd - t0) * 100) / 100,
|
|
464
|
+
);
|
|
465
|
+
span.setAttribute("canvasWidth", this.canvasElement.width);
|
|
466
|
+
span.setAttribute("canvasHeight", this.canvasElement.height);
|
|
467
|
+
|
|
468
|
+
log("trace: frame drawn to canvas", { seekToMs });
|
|
469
|
+
},
|
|
470
|
+
);
|
|
369
471
|
}
|
|
370
472
|
|
|
371
473
|
/**
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import { type ExportResult, ExportResultCode } from "@opentelemetry/core";
|
|
2
|
+
import type { ReadableSpan, SpanExporter } from "@opentelemetry/sdk-trace-base";
|
|
3
|
+
|
|
4
|
+
function toHex(value: unknown): string {
|
|
5
|
+
if (typeof value === "string") return value;
|
|
6
|
+
if (Array.isArray(value)) {
|
|
7
|
+
return value
|
|
8
|
+
.map((b) => {
|
|
9
|
+
const byte = typeof b === "number" ? b : 0;
|
|
10
|
+
return byte.toString(16).padStart(2, "0");
|
|
11
|
+
})
|
|
12
|
+
.join("");
|
|
13
|
+
}
|
|
14
|
+
if (ArrayBuffer.isView(value)) {
|
|
15
|
+
return Array.from(value as Uint8Array)
|
|
16
|
+
.map((b) => b.toString(16).padStart(2, "0"))
|
|
17
|
+
.join("");
|
|
18
|
+
}
|
|
19
|
+
return String(value);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
interface OtlpAttributeValue {
|
|
23
|
+
stringValue?: string;
|
|
24
|
+
intValue?: number;
|
|
25
|
+
doubleValue?: number;
|
|
26
|
+
boolValue?: boolean;
|
|
27
|
+
arrayValue?: { values: OtlpAttributeValue[] };
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function convertAttribute(value: unknown): OtlpAttributeValue {
|
|
31
|
+
if (typeof value === "string") return { stringValue: value };
|
|
32
|
+
if (typeof value === "number")
|
|
33
|
+
return Number.isInteger(value)
|
|
34
|
+
? { intValue: value }
|
|
35
|
+
: { doubleValue: value };
|
|
36
|
+
if (typeof value === "boolean") return { boolValue: value };
|
|
37
|
+
if (Array.isArray(value))
|
|
38
|
+
return { arrayValue: { values: value.map(convertAttribute) } };
|
|
39
|
+
return { stringValue: String(value) };
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
interface BridgeWithSpanExport {
|
|
43
|
+
exportSpans?: (endpoint: string, payload: string) => void;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export class BridgeSpanExporter implements SpanExporter {
|
|
47
|
+
private bridge: BridgeWithSpanExport;
|
|
48
|
+
private endpoint: string;
|
|
49
|
+
|
|
50
|
+
constructor(bridge: BridgeWithSpanExport, endpoint: string) {
|
|
51
|
+
this.bridge = bridge;
|
|
52
|
+
this.endpoint = endpoint;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export(
|
|
56
|
+
spans: ReadableSpan[],
|
|
57
|
+
resultCallback: (result: ExportResult) => void,
|
|
58
|
+
): void {
|
|
59
|
+
if (!this.bridge?.exportSpans) {
|
|
60
|
+
resultCallback({ code: ExportResultCode.FAILED });
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
try {
|
|
65
|
+
const otlpPayload = {
|
|
66
|
+
resourceSpans: [
|
|
67
|
+
{
|
|
68
|
+
resource: {
|
|
69
|
+
attributes: Object.entries(
|
|
70
|
+
spans[0]?.resource?.attributes || {},
|
|
71
|
+
).map(([key, value]) => ({
|
|
72
|
+
key,
|
|
73
|
+
value: convertAttribute(value),
|
|
74
|
+
})),
|
|
75
|
+
},
|
|
76
|
+
scopeSpans: [
|
|
77
|
+
{
|
|
78
|
+
scope: {
|
|
79
|
+
name: "telecine-browser",
|
|
80
|
+
version: "1.0.0",
|
|
81
|
+
},
|
|
82
|
+
spans: spans.map((span) => {
|
|
83
|
+
const ctx = span.spanContext();
|
|
84
|
+
return {
|
|
85
|
+
traceId: toHex(ctx.traceId),
|
|
86
|
+
spanId: toHex(ctx.spanId),
|
|
87
|
+
parentSpanId: span.parentSpanId
|
|
88
|
+
? toHex(span.parentSpanId)
|
|
89
|
+
: undefined,
|
|
90
|
+
name: span.name,
|
|
91
|
+
kind: span.kind,
|
|
92
|
+
startTimeUnixNano: String(
|
|
93
|
+
span.startTime[0] * 1_000_000_000 + span.startTime[1],
|
|
94
|
+
),
|
|
95
|
+
endTimeUnixNano: String(
|
|
96
|
+
span.endTime[0] * 1_000_000_000 + span.endTime[1],
|
|
97
|
+
),
|
|
98
|
+
attributes: Object.entries(span.attributes).map(
|
|
99
|
+
([key, value]) => ({
|
|
100
|
+
key,
|
|
101
|
+
value: convertAttribute(value),
|
|
102
|
+
}),
|
|
103
|
+
),
|
|
104
|
+
status: span.status,
|
|
105
|
+
events: span.events.map((event) => ({
|
|
106
|
+
timeUnixNano: String(
|
|
107
|
+
event.time[0] * 1_000_000_000 + event.time[1],
|
|
108
|
+
),
|
|
109
|
+
name: event.name,
|
|
110
|
+
attributes: Object.entries(event.attributes || {}).map(
|
|
111
|
+
([key, value]) => ({
|
|
112
|
+
key,
|
|
113
|
+
value: convertAttribute(value),
|
|
114
|
+
}),
|
|
115
|
+
),
|
|
116
|
+
})),
|
|
117
|
+
links: span.links.map((link) => ({
|
|
118
|
+
traceId: toHex(link.context.traceId),
|
|
119
|
+
spanId: toHex(link.context.spanId),
|
|
120
|
+
attributes: Object.entries(link.attributes || {}).map(
|
|
121
|
+
([key, value]) => ({
|
|
122
|
+
key,
|
|
123
|
+
value: convertAttribute(value),
|
|
124
|
+
}),
|
|
125
|
+
),
|
|
126
|
+
})),
|
|
127
|
+
};
|
|
128
|
+
}),
|
|
129
|
+
},
|
|
130
|
+
],
|
|
131
|
+
},
|
|
132
|
+
],
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
const serializedPayload = JSON.stringify(otlpPayload);
|
|
136
|
+
|
|
137
|
+
this.bridge.exportSpans(this.endpoint, serializedPayload);
|
|
138
|
+
resultCallback({ code: ExportResultCode.SUCCESS });
|
|
139
|
+
} catch (error) {
|
|
140
|
+
resultCallback({
|
|
141
|
+
code: ExportResultCode.FAILED,
|
|
142
|
+
error: error instanceof Error ? error : new Error(String(error)),
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
shutdown(): Promise<void> {
|
|
148
|
+
return Promise.resolve();
|
|
149
|
+
}
|
|
150
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { ZoneContextManager } from "@opentelemetry/context-zone";
|
|
2
|
+
import { Resource } from "@opentelemetry/resources";
|
|
3
|
+
import {
|
|
4
|
+
BatchSpanProcessor,
|
|
5
|
+
SimpleSpanProcessor,
|
|
6
|
+
} from "@opentelemetry/sdk-trace-base";
|
|
7
|
+
import { WebTracerProvider } from "@opentelemetry/sdk-trace-web";
|
|
8
|
+
import { ATTR_SERVICE_NAME } from "@opentelemetry/semantic-conventions";
|
|
9
|
+
import { BridgeSpanExporter } from "./BridgeSpanExporter.js";
|
|
10
|
+
|
|
11
|
+
let isInitialized = false;
|
|
12
|
+
let provider: WebTracerProvider | null = null;
|
|
13
|
+
|
|
14
|
+
interface BridgeWithSpanExport {
|
|
15
|
+
exportSpans?: (endpoint: string, payload: string) => void;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface BrowserTracingConfig {
|
|
19
|
+
otelEndpoint: string;
|
|
20
|
+
serviceName?: string;
|
|
21
|
+
bridge?: BridgeWithSpanExport;
|
|
22
|
+
useBatching?: boolean;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function setupBrowserTracing(config: BrowserTracingConfig): void {
|
|
26
|
+
if (isInitialized) {
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
try {
|
|
31
|
+
if (!config.bridge) {
|
|
32
|
+
throw new Error("Bridge is required for browser tracing");
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const exporter = new BridgeSpanExporter(config.bridge, config.otelEndpoint);
|
|
36
|
+
|
|
37
|
+
// Use batching to reduce overhead (spans exported in groups rather than one-by-one)
|
|
38
|
+
const spanProcessor = config.useBatching
|
|
39
|
+
? new BatchSpanProcessor(exporter, {
|
|
40
|
+
maxQueueSize: 100,
|
|
41
|
+
maxExportBatchSize: 10,
|
|
42
|
+
scheduledDelayMillis: 500,
|
|
43
|
+
})
|
|
44
|
+
: new SimpleSpanProcessor(exporter);
|
|
45
|
+
|
|
46
|
+
provider = new WebTracerProvider({
|
|
47
|
+
resource: new Resource({
|
|
48
|
+
[ATTR_SERVICE_NAME]: config.serviceName || "telecine-browser",
|
|
49
|
+
}),
|
|
50
|
+
spanProcessors: [spanProcessor],
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
// Register with ZoneContextManager for browser context propagation
|
|
54
|
+
// ZoneContextManager includes zone.js for async context tracking
|
|
55
|
+
provider.register({
|
|
56
|
+
contextManager: new ZoneContextManager(),
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
isInitialized = true;
|
|
60
|
+
} catch (error) {
|
|
61
|
+
console.error("Failed to initialize browser tracing:", error);
|
|
62
|
+
throw error;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function isBrowserTracingInitialized(): boolean {
|
|
67
|
+
return isInitialized;
|
|
68
|
+
}
|