@sanseng/liveavatar-js-sdk-skywalking 1.0.0 → 1.0.1

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 CHANGED
@@ -4,8 +4,9 @@
4
4
 
5
5
  ## What it solves
6
6
 
7
- - Reports LiveAvatar A/V Sync alert lifecycles with compact, causally ordered diagnostic evidence for investigation in SkyWalking.
7
+ - Reports SDK-generated LiveAvatar A/V Sync alert lifecycles with compact, causally ordered diagnostic evidence for investigation in SkyWalking.
8
8
  - Reports qualifying SDK Operational Log errors without uploading the original error message, stack, token, or business payload.
9
+ - Reports every terminal `performance.first-frame` Operational Log, including success, connection failure, and first-frame timeout.
9
10
  - Adds observability through the public LiveAvatar Plugin API v1, without subscribing to legacy SDK events or using SDK internals.
10
11
  - Bundles and registers a browser SkyWalking client when one has not already been supplied by the application.
11
12
 
@@ -19,24 +20,40 @@ npm install @sanseng/liveavatar-js-sdk @sanseng/liveavatar-js-sdk-skywalking
19
20
  import { createClient } from "@sanseng/liveavatar-js-sdk";
20
21
  import { createSkyWalkingPlugin } from "@sanseng/liveavatar-js-sdk-skywalking";
21
22
 
23
+ const client = createClient({
24
+ plugins: [createSkyWalkingPlugin()],
25
+ connectConfig: {
26
+ // Your normal LiveAvatar connection configuration.
27
+ },
28
+ });
29
+ ```
30
+
31
+ The plugin registers SkyWalking during client setup. SDK errors, terminal first-frame logs, and SDK-generated A/V Sync alerts are all reported by default. Its service is always `yy-tx::liveavatar-js-sdk`; it cannot be configured. If `window.ClientMonitor` already exists, the plugin treats it as externally registered and never overwrites or registers it. The bundled client registers only once per page; later differing configurations are ignored with a console warning.
32
+
33
+ Enable SDK A/V Sync diagnostics only when automatic A/V Sync alerts and their evidence are needed. When it is disabled, the SDK produces no automatic A/V Sync alerts, so the plugin does not query A/V telemetry or send A/V logs:
34
+
35
+ ```ts
22
36
  const client = createClient({
23
37
  debug: { avSync: { enabled: true } },
24
- plugins: [
25
- createSkyWalkingPlugin({
26
- isOverseas: false,
27
- serviceVersion: "web_1.0.0",
28
- jsErrors: true,
29
- apiErrors: true,
30
- resourceErrors: true,
31
- autoTracePerf: true,
32
- }),
33
- ],
38
+ plugins: [createSkyWalkingPlugin()],
34
39
  connectConfig: {
35
40
  // Your normal LiveAvatar connection configuration.
36
41
  },
37
42
  });
38
43
  ```
39
44
 
40
- The plugin registers SkyWalking during client setup. Its service is always `yy-tx::liveavatar-js-sdk`; it cannot be configured. If `window.ClientMonitor` already exists, the plugin treats it as externally registered and never overwrites or registers it. The bundled client registers only once per page; later differing configurations are ignored with a console warning.
45
+ ## Reporting controls
46
+
47
+ Use `reporting` to disable individual automatic report types; every type defaults to `true`:
48
+
49
+ ```ts
50
+ createSkyWalkingPlugin({
51
+ reporting: { sdkError: true, firstFrame: false, avSync: true },
52
+ });
53
+ ```
54
+
55
+ When all three report types are `false`, the plugin does not initialize SkyWalking or subscribe to SDK extension points.
56
+
57
+ Each SDK `performance.first-frame` Operational Log produces one SkyWalking structured log through `reportFrameErrors` with a one-millisecond dispatch timeout. “First frame” means the SDK-owned private video element reached `loadeddata`; it does not claim that pixels have already appeared in the application's visible UI. See the [API reference](docs/api.en.md#first-frame-log) for outcomes, payload fields, and omission rules.
41
58
 
42
- The package requires `@sanseng/liveavatar-js-sdk` `^2.2.0`.
59
+ The package requires `@sanseng/liveavatar-js-sdk` `^2.4.0`.
package/dist/index.cjs.js CHANGED
@@ -46,28 +46,34 @@ class SkyWalkingPlugin {
46
46
  this.apiVersion = 1;
47
47
  this._nextInstallationGeneration = 0;
48
48
  this._skyWalkingConfig = { ...config };
49
+ this._reporting = normalizeReportingConfig(config.reporting);
49
50
  }
50
51
  setup(registrar) {
51
52
  this._deactivateInstallation();
53
+ if (!hasEnabledReporting(this._reporting))
54
+ return;
52
55
  registerSkyWalking(this._skyWalkingConfig);
53
- const diagnostics = registrar.consume(liveavatarJsSdk.SDK_AVSYNC_DIAGNOSTICS_V1);
54
56
  const installation = {
55
57
  generation: ++this._nextInstallationGeneration,
56
- diagnostics,
57
58
  abortController: new AbortController(),
58
59
  alertReportStates: new Map(),
59
60
  active: true,
60
61
  };
61
62
  this._installation = installation;
62
63
  try {
63
- registrar.observe(liveavatarJsSdk.SDK_AVSYNC_ALERTS_V1, {
64
- id: AVSYNC_ALERTS_CONTRIBUTION_ID,
65
- handler: (alert, context) => this._handleAlertNotification(installation, alert, context.signal),
66
- });
67
- registrar.observe(liveavatarJsSdk.SDK_OPERATIONAL_LOGS_V1, {
68
- id: OPERATIONAL_LOGS_CONTRIBUTION_ID,
69
- handler: (record, context) => this._handleOperationalLog(installation, record, context.signal),
70
- });
64
+ if (this._reporting.avSync) {
65
+ installation.diagnostics = registrar.consume(liveavatarJsSdk.SDK_AVSYNC_DIAGNOSTICS_V1);
66
+ registrar.observe(liveavatarJsSdk.SDK_AVSYNC_ALERTS_V1, {
67
+ id: AVSYNC_ALERTS_CONTRIBUTION_ID,
68
+ handler: (alert, context) => this._handleAlertNotification(installation, alert, context.signal),
69
+ });
70
+ }
71
+ if (this._reporting.sdkError || this._reporting.firstFrame) {
72
+ registrar.observe(liveavatarJsSdk.SDK_OPERATIONAL_LOGS_V1, {
73
+ id: OPERATIONAL_LOGS_CONTRIBUTION_ID,
74
+ handler: (record, context) => this._handleOperationalLog(installation, record, context.signal),
75
+ });
76
+ }
71
77
  }
72
78
  catch (error) {
73
79
  this._deactivateInstallation();
@@ -116,12 +122,19 @@ class SkyWalkingPlugin {
116
122
  }
117
123
  }
118
124
  _handleOperationalLog(installation, record, signal) {
119
- if (!this._isInvocationActive(installation, signal) ||
120
- record.level !== "error" ||
121
- record.event !== "sdk.error") {
125
+ if (!this._isInvocationActive(installation, signal))
122
126
  return Promise.resolve();
127
+ if (this._reporting.sdkError &&
128
+ record.level === "error" &&
129
+ record.event === "sdk.error") {
130
+ return this._runObserverTask(installation, signal, "SDK error log", () => this._reportSdkError(installation, record, signal));
123
131
  }
124
- return this._runObserverTask(installation, signal, "SDK error log", () => this._reportSdkError(installation, record, signal));
132
+ if (this._reporting.firstFrame &&
133
+ record.category === "media" &&
134
+ record.event === "performance.first-frame") {
135
+ return this._runObserverTask(installation, signal, "first-frame log", () => this._reportFirstFrameLog(installation, record, signal));
136
+ }
137
+ return Promise.resolve();
125
138
  }
126
139
  _enqueue(state, task) {
127
140
  const queued = state.tail.then(task, task);
@@ -132,6 +145,9 @@ class SkyWalkingPlugin {
132
145
  const isActive = () => this._isInvocationActive(installation, signal);
133
146
  if (!isActive())
134
147
  return;
148
+ const diagnostics = installation.diagnostics;
149
+ if (diagnostics === undefined)
150
+ return;
135
151
  const clientMonitor = getClientMonitor();
136
152
  if (clientMonitor === undefined) {
137
153
  this._warnInvocation(installation, signal, "ClientMonitor.reportFrameErrors is unavailable", {
@@ -144,7 +160,7 @@ class SkyWalkingPlugin {
144
160
  const query = await createEvidenceQuery(alert.phase, state.lastSuccessfulToWallClockMs, [signal, installation.abortController.signal]);
145
161
  if (!isActive() || query === undefined)
146
162
  return;
147
- const collection = await collectTelemetry(installation.diagnostics, alert, retrievedAtWallClockMs, query, isActive, (message, error) => this._warnInvocationFailure(installation, signal, message, error, {
163
+ const collection = await collectTelemetry(diagnostics, alert, retrievedAtWallClockMs, query, isActive, (message, error) => this._warnInvocationFailure(installation, signal, message, error, {
148
164
  alert: toJsonSafeAlert(alert),
149
165
  }));
150
166
  if (!isActive() || collection === undefined)
@@ -272,6 +288,43 @@ class SkyWalkingPlugin {
272
288
  if (!this._isInvocationActive(installation, signal))
273
289
  return;
274
290
  }
291
+ async _reportFirstFrameLog(installation, record, signal) {
292
+ if (!this._isInvocationActive(installation, signal))
293
+ return;
294
+ const payload = toSkyWalkingFirstFrameLogPayload(record);
295
+ if (payload === undefined) {
296
+ this._warnInvocation(installation, signal, "Skipped invalid first-frame Operational Log", { eventId: record.eventId });
297
+ return;
298
+ }
299
+ const clientMonitor = getClientMonitor();
300
+ if (clientMonitor === undefined) {
301
+ this._warnInvocation(installation, signal, "ClientMonitor.reportFrameErrors is unavailable", {
302
+ pluginId: this.id,
303
+ attemptId: payload.attemptId,
304
+ outcome: payload.outcome,
305
+ });
306
+ return;
307
+ }
308
+ if (!this._isInvocationActive(installation, signal))
309
+ return;
310
+ try {
311
+ await Promise.resolve(clientMonitor.reportFrameErrors({
312
+ category: "media",
313
+ grade: payload.outcome === "first-frame" ? "Info" : "Warning",
314
+ content: payload,
315
+ // The bundled client batches error records. Its public timeout
316
+ // option bypasses the default delay for these terminal events.
317
+ timeout: 1,
318
+ }, {
319
+ name: "",
320
+ message: `[LiveAvatar first-frame] ${payload.outcome}`,
321
+ stack: "",
322
+ }));
323
+ }
324
+ catch (error) {
325
+ this._warnInvocationFailure(installation, signal, "Failed to report first-frame Operational Log to SkyWalking", error, { attemptId: payload.attemptId, outcome: payload.outcome });
326
+ }
327
+ }
275
328
  _deactivateInstallation() {
276
329
  const installation = this._installation;
277
330
  if (installation === undefined)
@@ -1231,6 +1284,60 @@ function toJsonSafeAlert(alert) {
1231
1284
  features: { ...alert.features },
1232
1285
  };
1233
1286
  }
1287
+ /**
1288
+ * Strictly projects the published first-frame Operational Log. The SDK record
1289
+ * has additional identifiers and arbitrary attributes; none of those can
1290
+ * reach SkyWalking through this path.
1291
+ */
1292
+ function toSkyWalkingFirstFrameLogPayload(record) {
1293
+ const correlation = toRecord(record.correlation);
1294
+ const attributes = toRecord(record.attributes);
1295
+ const attemptId = correlation.attemptId;
1296
+ const outcome = attributes.outcome;
1297
+ if (!isNonNegativeNumber(record.occurredAtMs) ||
1298
+ !isPositiveInteger(attemptId) ||
1299
+ !isFirstFrameOutcome(outcome)) {
1300
+ return undefined;
1301
+ }
1302
+ const sourceNodes = toRecord(attributes.nodes);
1303
+ const nodes = {};
1304
+ copyNonNegativeNumber(sourceNodes, "configReadyMs", nodes);
1305
+ copyNonNegativeNumber(sourceNodes, "rtcConnectedMs", nodes);
1306
+ copyNonNegativeNumber(sourceNodes, "videoTrackAddedMs", nodes);
1307
+ copyNonNegativeNumber(sourceNodes, "videoMetadataMs", nodes);
1308
+ copyNonNegativeNumber(sourceNodes, "firstFrameMs", nodes);
1309
+ return {
1310
+ schemaVersion: 1,
1311
+ kind: "liveavatar.performance.first-frame",
1312
+ sdkVersion: liveavatarJsSdk.VERSION,
1313
+ occurredAtMs: record.occurredAtMs,
1314
+ attemptId,
1315
+ outcome,
1316
+ ...(typeof attributes.renderMode === "string"
1317
+ ? { renderMode: attributes.renderMode }
1318
+ : {}),
1319
+ ...(isNonNegativeNumber(attributes.timeoutAfterRtcMs)
1320
+ ? { timeoutAfterRtcMs: attributes.timeoutAfterRtcMs }
1321
+ : {}),
1322
+ nodes,
1323
+ };
1324
+ }
1325
+ function copyNonNegativeNumber(source, key, target) {
1326
+ const value = source[key];
1327
+ if (isNonNegativeNumber(value))
1328
+ target[key] = value;
1329
+ }
1330
+ function isFirstFrameOutcome(value) {
1331
+ return (value === "first-frame" ||
1332
+ value === "connect-failed" ||
1333
+ value === "first-frame-timeout");
1334
+ }
1335
+ function isPositiveInteger(value) {
1336
+ return typeof value === "number" && Number.isInteger(value) && value > 0;
1337
+ }
1338
+ function isNonNegativeNumber(value) {
1339
+ return typeof value === "number" && Number.isFinite(value) && value >= 0;
1340
+ }
1234
1341
  function stringifyJson(value) {
1235
1342
  const serialized = JSON.stringify(value);
1236
1343
  if (serialized === undefined) {
@@ -1279,9 +1386,8 @@ function registerSkyWalking(config = {}) {
1279
1386
  }
1280
1387
  /** Triggers a SkyWalking performance report through an already registered client. */
1281
1388
  function setSkyWalkingPerformance(config = {}, customMetrics) {
1282
- const clientMonitor = getClientMonitor();
1283
- if (clientMonitor === undefined ||
1284
- typeof clientMonitor.setPerformance !== "function") {
1389
+ const clientMonitor = getPerformanceClientMonitor();
1390
+ if (clientMonitor === undefined) {
1285
1391
  warnOnce("set-performance-unavailable", "[LiveAvatar SkyWalking plugin] ClientMonitor.setPerformance is unavailable; register SkyWalking first");
1286
1392
  return;
1287
1393
  }
@@ -1306,6 +1412,16 @@ async function reportSkyWalkingFrameErrors(config, error) {
1306
1412
  }
1307
1413
  await Promise.resolve(clientMonitor.reportFrameErrors(config, error));
1308
1414
  }
1415
+ function normalizeReportingConfig(config) {
1416
+ return {
1417
+ sdkError: config?.sdkError ?? true,
1418
+ firstFrame: config?.firstFrame ?? true,
1419
+ avSync: config?.avSync ?? true,
1420
+ };
1421
+ }
1422
+ function hasEnabledReporting(config) {
1423
+ return config.sdkError || config.firstFrame || config.avSync;
1424
+ }
1309
1425
  function normalizeSkyWalkingConfig(config) {
1310
1426
  return {
1311
1427
  collector: config.collector ??
@@ -1353,6 +1469,13 @@ function getClientMonitor() {
1353
1469
  ? candidate
1354
1470
  : undefined;
1355
1471
  }
1472
+ /** Finds the performance API without requiring the unrelated error API. */
1473
+ function getPerformanceClientMonitor() {
1474
+ const candidate = getGlobalClientMonitor();
1475
+ return typeof candidate?.setPerformance === "function"
1476
+ ? candidate
1477
+ : undefined;
1478
+ }
1356
1479
  function getGlobalClientMonitor() {
1357
1480
  if (typeof window === "undefined")
1358
1481
  return undefined;
package/dist/index.d.ts CHANGED
@@ -14,11 +14,23 @@ interface SkyWalkingSlowInteractionConfig {
14
14
  threshold?: number;
15
15
  reportInterval?: number;
16
16
  }
17
+ /** Selects which SDK-generated records the plugin sends to SkyWalking. */
18
+ interface SkyWalkingReportingConfig {
19
+ /** Report SDK Operational Log errors. Defaults to `true`. */
20
+ sdkError?: boolean;
21
+ /** Report terminal SDK first-frame Operational Logs. Defaults to `true`. */
22
+ firstFrame?: boolean;
23
+ /** Report SDK-generated A/V Sync alert lifecycles and evidence. Defaults to `true`. */
24
+ avSync?: boolean;
25
+ }
17
26
  /**
18
27
  * Registration options accepted by SkyWalking js-sdk 1.1.27, plus the
19
- * plugin-level overseas switch. `service` is deliberately fixed by the plugin.
28
+ * plugin-level reporting controls and overseas switch. `service` is deliberately
29
+ * fixed by the plugin.
20
30
  */
21
31
  interface SkyWalkingPluginConfig {
32
+ /** Select automatic SDK log types. Every type is enabled by default. */
33
+ reporting?: SkyWalkingReportingConfig;
22
34
  /** Explicit collector URL. It takes precedence over `isOverseas`. */
23
35
  collector?: string;
24
36
  /** `false` selects the domestic collector; `true` selects the overseas one. */
@@ -241,6 +253,31 @@ interface SkyWalkingFrameErrorReportConfig {
241
253
  maxReqCostTime?: number;
242
254
  timeout?: number;
243
255
  }
256
+ /**
257
+ * Custom performance fields sent for one terminal connect-to-first-frame
258
+ * attempt. Stage fields are present only when the SDK observed that stage.
259
+ */
260
+ type SkyWalkingFirstFrameOutcome = "first-frame" | "connect-failed" | "first-frame-timeout";
261
+ /** Allowed first-frame timing nodes copied from the SDK Operational Log. */
262
+ interface SkyWalkingFirstFrameLogNodes {
263
+ configReadyMs?: number;
264
+ rtcConnectedMs?: number;
265
+ videoTrackAddedMs?: number;
266
+ videoMetadataMs?: number;
267
+ firstFrameMs?: number;
268
+ }
269
+ /** Structured content of one terminal SDK `performance.first-frame` log. */
270
+ interface SkyWalkingFirstFrameLogPayload {
271
+ schemaVersion: 1;
272
+ kind: "liveavatar.performance.first-frame";
273
+ sdkVersion: string;
274
+ occurredAtMs: number;
275
+ attemptId: number;
276
+ outcome: SkyWalkingFirstFrameOutcome;
277
+ renderMode?: string;
278
+ timeoutAfterRtcMs?: number;
279
+ nodes: SkyWalkingFirstFrameLogNodes;
280
+ }
244
281
  /**
245
282
  * Reports LiveAvatar A/V-sync incident lifecycle events through the bundled or
246
283
  * externally provided SkyWalking client.
@@ -250,6 +287,7 @@ declare class SkyWalkingPlugin implements SDKPlugin {
250
287
  /** SDK Plugin API v1 is the registrar-based public plugin contract. */
251
288
  readonly apiVersion = 1;
252
289
  private readonly _skyWalkingConfig;
290
+ private readonly _reporting;
253
291
  private _installation;
254
292
  private _nextInstallationGeneration;
255
293
  constructor(config?: SkyWalkingPluginConfig);
@@ -261,6 +299,7 @@ declare class SkyWalkingPlugin implements SDKPlugin {
261
299
  private _reportActiveAlert;
262
300
  private _reportRecovery;
263
301
  private _reportSdkError;
302
+ private _reportFirstFrameLog;
264
303
  private _deactivateInstallation;
265
304
  private _isInvocationActive;
266
305
  private _runObserverTask;
@@ -283,4 +322,4 @@ declare function setSkyWalkingHRT(config?: Partial<SkyWalkingPluginConfig>, cust
283
322
  declare function reportSkyWalkingFrameErrors(config: SkyWalkingFrameErrorReportConfig, error: SkyWalkingFrameError): Promise<void>;
284
323
 
285
324
  export { SKYWALKING_PLUGIN_ID, SkyWalkingPlugin, createSkyWalkingPlugin, registerSkyWalking, reportSkyWalkingFrameErrors, setSkyWalkingHRT, setSkyWalkingPerformance };
286
- export type { AvSyncAlertEvidencePayload, AvSyncEvidence, AvSyncEvidenceLogChunkPayload, AvSyncRecoveryPayload, JsonSafeAlert, SkyWalkingFrameError, SkyWalkingFrameErrorReportConfig, SkyWalkingHooks, SkyWalkingPluginConfig, SkyWalkingRegistrationOptions, SkyWalkingSdkErrorPayload, SkyWalkingSlowInteractionConfig };
325
+ export type { AvSyncAlertEvidencePayload, AvSyncEvidence, AvSyncEvidenceLogChunkPayload, AvSyncRecoveryPayload, JsonSafeAlert, SkyWalkingFirstFrameLogNodes, SkyWalkingFirstFrameLogPayload, SkyWalkingFirstFrameOutcome, SkyWalkingFrameError, SkyWalkingFrameErrorReportConfig, SkyWalkingHooks, SkyWalkingPluginConfig, SkyWalkingRegistrationOptions, SkyWalkingReportingConfig, SkyWalkingSdkErrorPayload, SkyWalkingSlowInteractionConfig };
package/dist/index.esm.js CHANGED
@@ -44,28 +44,34 @@ class SkyWalkingPlugin {
44
44
  this.apiVersion = 1;
45
45
  this._nextInstallationGeneration = 0;
46
46
  this._skyWalkingConfig = { ...config };
47
+ this._reporting = normalizeReportingConfig(config.reporting);
47
48
  }
48
49
  setup(registrar) {
49
50
  this._deactivateInstallation();
51
+ if (!hasEnabledReporting(this._reporting))
52
+ return;
50
53
  registerSkyWalking(this._skyWalkingConfig);
51
- const diagnostics = registrar.consume(SDK_AVSYNC_DIAGNOSTICS_V1);
52
54
  const installation = {
53
55
  generation: ++this._nextInstallationGeneration,
54
- diagnostics,
55
56
  abortController: new AbortController(),
56
57
  alertReportStates: new Map(),
57
58
  active: true,
58
59
  };
59
60
  this._installation = installation;
60
61
  try {
61
- registrar.observe(SDK_AVSYNC_ALERTS_V1, {
62
- id: AVSYNC_ALERTS_CONTRIBUTION_ID,
63
- handler: (alert, context) => this._handleAlertNotification(installation, alert, context.signal),
64
- });
65
- registrar.observe(SDK_OPERATIONAL_LOGS_V1, {
66
- id: OPERATIONAL_LOGS_CONTRIBUTION_ID,
67
- handler: (record, context) => this._handleOperationalLog(installation, record, context.signal),
68
- });
62
+ if (this._reporting.avSync) {
63
+ installation.diagnostics = registrar.consume(SDK_AVSYNC_DIAGNOSTICS_V1);
64
+ registrar.observe(SDK_AVSYNC_ALERTS_V1, {
65
+ id: AVSYNC_ALERTS_CONTRIBUTION_ID,
66
+ handler: (alert, context) => this._handleAlertNotification(installation, alert, context.signal),
67
+ });
68
+ }
69
+ if (this._reporting.sdkError || this._reporting.firstFrame) {
70
+ registrar.observe(SDK_OPERATIONAL_LOGS_V1, {
71
+ id: OPERATIONAL_LOGS_CONTRIBUTION_ID,
72
+ handler: (record, context) => this._handleOperationalLog(installation, record, context.signal),
73
+ });
74
+ }
69
75
  }
70
76
  catch (error) {
71
77
  this._deactivateInstallation();
@@ -114,12 +120,19 @@ class SkyWalkingPlugin {
114
120
  }
115
121
  }
116
122
  _handleOperationalLog(installation, record, signal) {
117
- if (!this._isInvocationActive(installation, signal) ||
118
- record.level !== "error" ||
119
- record.event !== "sdk.error") {
123
+ if (!this._isInvocationActive(installation, signal))
120
124
  return Promise.resolve();
125
+ if (this._reporting.sdkError &&
126
+ record.level === "error" &&
127
+ record.event === "sdk.error") {
128
+ return this._runObserverTask(installation, signal, "SDK error log", () => this._reportSdkError(installation, record, signal));
121
129
  }
122
- return this._runObserverTask(installation, signal, "SDK error log", () => this._reportSdkError(installation, record, signal));
130
+ if (this._reporting.firstFrame &&
131
+ record.category === "media" &&
132
+ record.event === "performance.first-frame") {
133
+ return this._runObserverTask(installation, signal, "first-frame log", () => this._reportFirstFrameLog(installation, record, signal));
134
+ }
135
+ return Promise.resolve();
123
136
  }
124
137
  _enqueue(state, task) {
125
138
  const queued = state.tail.then(task, task);
@@ -130,6 +143,9 @@ class SkyWalkingPlugin {
130
143
  const isActive = () => this._isInvocationActive(installation, signal);
131
144
  if (!isActive())
132
145
  return;
146
+ const diagnostics = installation.diagnostics;
147
+ if (diagnostics === undefined)
148
+ return;
133
149
  const clientMonitor = getClientMonitor();
134
150
  if (clientMonitor === undefined) {
135
151
  this._warnInvocation(installation, signal, "ClientMonitor.reportFrameErrors is unavailable", {
@@ -142,7 +158,7 @@ class SkyWalkingPlugin {
142
158
  const query = await createEvidenceQuery(alert.phase, state.lastSuccessfulToWallClockMs, [signal, installation.abortController.signal]);
143
159
  if (!isActive() || query === undefined)
144
160
  return;
145
- const collection = await collectTelemetry(installation.diagnostics, alert, retrievedAtWallClockMs, query, isActive, (message, error) => this._warnInvocationFailure(installation, signal, message, error, {
161
+ const collection = await collectTelemetry(diagnostics, alert, retrievedAtWallClockMs, query, isActive, (message, error) => this._warnInvocationFailure(installation, signal, message, error, {
146
162
  alert: toJsonSafeAlert(alert),
147
163
  }));
148
164
  if (!isActive() || collection === undefined)
@@ -270,6 +286,43 @@ class SkyWalkingPlugin {
270
286
  if (!this._isInvocationActive(installation, signal))
271
287
  return;
272
288
  }
289
+ async _reportFirstFrameLog(installation, record, signal) {
290
+ if (!this._isInvocationActive(installation, signal))
291
+ return;
292
+ const payload = toSkyWalkingFirstFrameLogPayload(record);
293
+ if (payload === undefined) {
294
+ this._warnInvocation(installation, signal, "Skipped invalid first-frame Operational Log", { eventId: record.eventId });
295
+ return;
296
+ }
297
+ const clientMonitor = getClientMonitor();
298
+ if (clientMonitor === undefined) {
299
+ this._warnInvocation(installation, signal, "ClientMonitor.reportFrameErrors is unavailable", {
300
+ pluginId: this.id,
301
+ attemptId: payload.attemptId,
302
+ outcome: payload.outcome,
303
+ });
304
+ return;
305
+ }
306
+ if (!this._isInvocationActive(installation, signal))
307
+ return;
308
+ try {
309
+ await Promise.resolve(clientMonitor.reportFrameErrors({
310
+ category: "media",
311
+ grade: payload.outcome === "first-frame" ? "Info" : "Warning",
312
+ content: payload,
313
+ // The bundled client batches error records. Its public timeout
314
+ // option bypasses the default delay for these terminal events.
315
+ timeout: 1,
316
+ }, {
317
+ name: "",
318
+ message: `[LiveAvatar first-frame] ${payload.outcome}`,
319
+ stack: "",
320
+ }));
321
+ }
322
+ catch (error) {
323
+ this._warnInvocationFailure(installation, signal, "Failed to report first-frame Operational Log to SkyWalking", error, { attemptId: payload.attemptId, outcome: payload.outcome });
324
+ }
325
+ }
273
326
  _deactivateInstallation() {
274
327
  const installation = this._installation;
275
328
  if (installation === undefined)
@@ -1229,6 +1282,60 @@ function toJsonSafeAlert(alert) {
1229
1282
  features: { ...alert.features },
1230
1283
  };
1231
1284
  }
1285
+ /**
1286
+ * Strictly projects the published first-frame Operational Log. The SDK record
1287
+ * has additional identifiers and arbitrary attributes; none of those can
1288
+ * reach SkyWalking through this path.
1289
+ */
1290
+ function toSkyWalkingFirstFrameLogPayload(record) {
1291
+ const correlation = toRecord(record.correlation);
1292
+ const attributes = toRecord(record.attributes);
1293
+ const attemptId = correlation.attemptId;
1294
+ const outcome = attributes.outcome;
1295
+ if (!isNonNegativeNumber(record.occurredAtMs) ||
1296
+ !isPositiveInteger(attemptId) ||
1297
+ !isFirstFrameOutcome(outcome)) {
1298
+ return undefined;
1299
+ }
1300
+ const sourceNodes = toRecord(attributes.nodes);
1301
+ const nodes = {};
1302
+ copyNonNegativeNumber(sourceNodes, "configReadyMs", nodes);
1303
+ copyNonNegativeNumber(sourceNodes, "rtcConnectedMs", nodes);
1304
+ copyNonNegativeNumber(sourceNodes, "videoTrackAddedMs", nodes);
1305
+ copyNonNegativeNumber(sourceNodes, "videoMetadataMs", nodes);
1306
+ copyNonNegativeNumber(sourceNodes, "firstFrameMs", nodes);
1307
+ return {
1308
+ schemaVersion: 1,
1309
+ kind: "liveavatar.performance.first-frame",
1310
+ sdkVersion: VERSION,
1311
+ occurredAtMs: record.occurredAtMs,
1312
+ attemptId,
1313
+ outcome,
1314
+ ...(typeof attributes.renderMode === "string"
1315
+ ? { renderMode: attributes.renderMode }
1316
+ : {}),
1317
+ ...(isNonNegativeNumber(attributes.timeoutAfterRtcMs)
1318
+ ? { timeoutAfterRtcMs: attributes.timeoutAfterRtcMs }
1319
+ : {}),
1320
+ nodes,
1321
+ };
1322
+ }
1323
+ function copyNonNegativeNumber(source, key, target) {
1324
+ const value = source[key];
1325
+ if (isNonNegativeNumber(value))
1326
+ target[key] = value;
1327
+ }
1328
+ function isFirstFrameOutcome(value) {
1329
+ return (value === "first-frame" ||
1330
+ value === "connect-failed" ||
1331
+ value === "first-frame-timeout");
1332
+ }
1333
+ function isPositiveInteger(value) {
1334
+ return typeof value === "number" && Number.isInteger(value) && value > 0;
1335
+ }
1336
+ function isNonNegativeNumber(value) {
1337
+ return typeof value === "number" && Number.isFinite(value) && value >= 0;
1338
+ }
1232
1339
  function stringifyJson(value) {
1233
1340
  const serialized = JSON.stringify(value);
1234
1341
  if (serialized === undefined) {
@@ -1277,9 +1384,8 @@ function registerSkyWalking(config = {}) {
1277
1384
  }
1278
1385
  /** Triggers a SkyWalking performance report through an already registered client. */
1279
1386
  function setSkyWalkingPerformance(config = {}, customMetrics) {
1280
- const clientMonitor = getClientMonitor();
1281
- if (clientMonitor === undefined ||
1282
- typeof clientMonitor.setPerformance !== "function") {
1387
+ const clientMonitor = getPerformanceClientMonitor();
1388
+ if (clientMonitor === undefined) {
1283
1389
  warnOnce("set-performance-unavailable", "[LiveAvatar SkyWalking plugin] ClientMonitor.setPerformance is unavailable; register SkyWalking first");
1284
1390
  return;
1285
1391
  }
@@ -1304,6 +1410,16 @@ async function reportSkyWalkingFrameErrors(config, error) {
1304
1410
  }
1305
1411
  await Promise.resolve(clientMonitor.reportFrameErrors(config, error));
1306
1412
  }
1413
+ function normalizeReportingConfig(config) {
1414
+ return {
1415
+ sdkError: config?.sdkError ?? true,
1416
+ firstFrame: config?.firstFrame ?? true,
1417
+ avSync: config?.avSync ?? true,
1418
+ };
1419
+ }
1420
+ function hasEnabledReporting(config) {
1421
+ return config.sdkError || config.firstFrame || config.avSync;
1422
+ }
1307
1423
  function normalizeSkyWalkingConfig(config) {
1308
1424
  return {
1309
1425
  collector: config.collector ??
@@ -1351,6 +1467,13 @@ function getClientMonitor() {
1351
1467
  ? candidate
1352
1468
  : undefined;
1353
1469
  }
1470
+ /** Finds the performance API without requiring the unrelated error API. */
1471
+ function getPerformanceClientMonitor() {
1472
+ const candidate = getGlobalClientMonitor();
1473
+ return typeof candidate?.setPerformance === "function"
1474
+ ? candidate
1475
+ : undefined;
1476
+ }
1354
1477
  function getGlobalClientMonitor() {
1355
1478
  if (typeof window === "undefined")
1356
1479
  return undefined;
package/dist/index.umd.js CHANGED
@@ -48,28 +48,34 @@
48
48
  this.apiVersion = 1;
49
49
  this._nextInstallationGeneration = 0;
50
50
  this._skyWalkingConfig = { ...config };
51
+ this._reporting = normalizeReportingConfig(config.reporting);
51
52
  }
52
53
  setup(registrar) {
53
54
  this._deactivateInstallation();
55
+ if (!hasEnabledReporting(this._reporting))
56
+ return;
54
57
  registerSkyWalking(this._skyWalkingConfig);
55
- const diagnostics = registrar.consume(liveavatarJsSdk.SDK_AVSYNC_DIAGNOSTICS_V1);
56
58
  const installation = {
57
59
  generation: ++this._nextInstallationGeneration,
58
- diagnostics,
59
60
  abortController: new AbortController(),
60
61
  alertReportStates: new Map(),
61
62
  active: true,
62
63
  };
63
64
  this._installation = installation;
64
65
  try {
65
- registrar.observe(liveavatarJsSdk.SDK_AVSYNC_ALERTS_V1, {
66
- id: AVSYNC_ALERTS_CONTRIBUTION_ID,
67
- handler: (alert, context) => this._handleAlertNotification(installation, alert, context.signal),
68
- });
69
- registrar.observe(liveavatarJsSdk.SDK_OPERATIONAL_LOGS_V1, {
70
- id: OPERATIONAL_LOGS_CONTRIBUTION_ID,
71
- handler: (record, context) => this._handleOperationalLog(installation, record, context.signal),
72
- });
66
+ if (this._reporting.avSync) {
67
+ installation.diagnostics = registrar.consume(liveavatarJsSdk.SDK_AVSYNC_DIAGNOSTICS_V1);
68
+ registrar.observe(liveavatarJsSdk.SDK_AVSYNC_ALERTS_V1, {
69
+ id: AVSYNC_ALERTS_CONTRIBUTION_ID,
70
+ handler: (alert, context) => this._handleAlertNotification(installation, alert, context.signal),
71
+ });
72
+ }
73
+ if (this._reporting.sdkError || this._reporting.firstFrame) {
74
+ registrar.observe(liveavatarJsSdk.SDK_OPERATIONAL_LOGS_V1, {
75
+ id: OPERATIONAL_LOGS_CONTRIBUTION_ID,
76
+ handler: (record, context) => this._handleOperationalLog(installation, record, context.signal),
77
+ });
78
+ }
73
79
  }
74
80
  catch (error) {
75
81
  this._deactivateInstallation();
@@ -118,12 +124,19 @@
118
124
  }
119
125
  }
120
126
  _handleOperationalLog(installation, record, signal) {
121
- if (!this._isInvocationActive(installation, signal) ||
122
- record.level !== "error" ||
123
- record.event !== "sdk.error") {
127
+ if (!this._isInvocationActive(installation, signal))
124
128
  return Promise.resolve();
129
+ if (this._reporting.sdkError &&
130
+ record.level === "error" &&
131
+ record.event === "sdk.error") {
132
+ return this._runObserverTask(installation, signal, "SDK error log", () => this._reportSdkError(installation, record, signal));
125
133
  }
126
- return this._runObserverTask(installation, signal, "SDK error log", () => this._reportSdkError(installation, record, signal));
134
+ if (this._reporting.firstFrame &&
135
+ record.category === "media" &&
136
+ record.event === "performance.first-frame") {
137
+ return this._runObserverTask(installation, signal, "first-frame log", () => this._reportFirstFrameLog(installation, record, signal));
138
+ }
139
+ return Promise.resolve();
127
140
  }
128
141
  _enqueue(state, task) {
129
142
  const queued = state.tail.then(task, task);
@@ -134,6 +147,9 @@
134
147
  const isActive = () => this._isInvocationActive(installation, signal);
135
148
  if (!isActive())
136
149
  return;
150
+ const diagnostics = installation.diagnostics;
151
+ if (diagnostics === undefined)
152
+ return;
137
153
  const clientMonitor = getClientMonitor();
138
154
  if (clientMonitor === undefined) {
139
155
  this._warnInvocation(installation, signal, "ClientMonitor.reportFrameErrors is unavailable", {
@@ -146,7 +162,7 @@
146
162
  const query = await createEvidenceQuery(alert.phase, state.lastSuccessfulToWallClockMs, [signal, installation.abortController.signal]);
147
163
  if (!isActive() || query === undefined)
148
164
  return;
149
- const collection = await collectTelemetry(installation.diagnostics, alert, retrievedAtWallClockMs, query, isActive, (message, error) => this._warnInvocationFailure(installation, signal, message, error, {
165
+ const collection = await collectTelemetry(diagnostics, alert, retrievedAtWallClockMs, query, isActive, (message, error) => this._warnInvocationFailure(installation, signal, message, error, {
150
166
  alert: toJsonSafeAlert(alert),
151
167
  }));
152
168
  if (!isActive() || collection === undefined)
@@ -274,6 +290,43 @@
274
290
  if (!this._isInvocationActive(installation, signal))
275
291
  return;
276
292
  }
293
+ async _reportFirstFrameLog(installation, record, signal) {
294
+ if (!this._isInvocationActive(installation, signal))
295
+ return;
296
+ const payload = toSkyWalkingFirstFrameLogPayload(record);
297
+ if (payload === undefined) {
298
+ this._warnInvocation(installation, signal, "Skipped invalid first-frame Operational Log", { eventId: record.eventId });
299
+ return;
300
+ }
301
+ const clientMonitor = getClientMonitor();
302
+ if (clientMonitor === undefined) {
303
+ this._warnInvocation(installation, signal, "ClientMonitor.reportFrameErrors is unavailable", {
304
+ pluginId: this.id,
305
+ attemptId: payload.attemptId,
306
+ outcome: payload.outcome,
307
+ });
308
+ return;
309
+ }
310
+ if (!this._isInvocationActive(installation, signal))
311
+ return;
312
+ try {
313
+ await Promise.resolve(clientMonitor.reportFrameErrors({
314
+ category: "media",
315
+ grade: payload.outcome === "first-frame" ? "Info" : "Warning",
316
+ content: payload,
317
+ // The bundled client batches error records. Its public timeout
318
+ // option bypasses the default delay for these terminal events.
319
+ timeout: 1,
320
+ }, {
321
+ name: "",
322
+ message: `[LiveAvatar first-frame] ${payload.outcome}`,
323
+ stack: "",
324
+ }));
325
+ }
326
+ catch (error) {
327
+ this._warnInvocationFailure(installation, signal, "Failed to report first-frame Operational Log to SkyWalking", error, { attemptId: payload.attemptId, outcome: payload.outcome });
328
+ }
329
+ }
277
330
  _deactivateInstallation() {
278
331
  const installation = this._installation;
279
332
  if (installation === undefined)
@@ -1233,6 +1286,60 @@
1233
1286
  features: { ...alert.features },
1234
1287
  };
1235
1288
  }
1289
+ /**
1290
+ * Strictly projects the published first-frame Operational Log. The SDK record
1291
+ * has additional identifiers and arbitrary attributes; none of those can
1292
+ * reach SkyWalking through this path.
1293
+ */
1294
+ function toSkyWalkingFirstFrameLogPayload(record) {
1295
+ const correlation = toRecord(record.correlation);
1296
+ const attributes = toRecord(record.attributes);
1297
+ const attemptId = correlation.attemptId;
1298
+ const outcome = attributes.outcome;
1299
+ if (!isNonNegativeNumber(record.occurredAtMs) ||
1300
+ !isPositiveInteger(attemptId) ||
1301
+ !isFirstFrameOutcome(outcome)) {
1302
+ return undefined;
1303
+ }
1304
+ const sourceNodes = toRecord(attributes.nodes);
1305
+ const nodes = {};
1306
+ copyNonNegativeNumber(sourceNodes, "configReadyMs", nodes);
1307
+ copyNonNegativeNumber(sourceNodes, "rtcConnectedMs", nodes);
1308
+ copyNonNegativeNumber(sourceNodes, "videoTrackAddedMs", nodes);
1309
+ copyNonNegativeNumber(sourceNodes, "videoMetadataMs", nodes);
1310
+ copyNonNegativeNumber(sourceNodes, "firstFrameMs", nodes);
1311
+ return {
1312
+ schemaVersion: 1,
1313
+ kind: "liveavatar.performance.first-frame",
1314
+ sdkVersion: liveavatarJsSdk.VERSION,
1315
+ occurredAtMs: record.occurredAtMs,
1316
+ attemptId,
1317
+ outcome,
1318
+ ...(typeof attributes.renderMode === "string"
1319
+ ? { renderMode: attributes.renderMode }
1320
+ : {}),
1321
+ ...(isNonNegativeNumber(attributes.timeoutAfterRtcMs)
1322
+ ? { timeoutAfterRtcMs: attributes.timeoutAfterRtcMs }
1323
+ : {}),
1324
+ nodes,
1325
+ };
1326
+ }
1327
+ function copyNonNegativeNumber(source, key, target) {
1328
+ const value = source[key];
1329
+ if (isNonNegativeNumber(value))
1330
+ target[key] = value;
1331
+ }
1332
+ function isFirstFrameOutcome(value) {
1333
+ return (value === "first-frame" ||
1334
+ value === "connect-failed" ||
1335
+ value === "first-frame-timeout");
1336
+ }
1337
+ function isPositiveInteger(value) {
1338
+ return typeof value === "number" && Number.isInteger(value) && value > 0;
1339
+ }
1340
+ function isNonNegativeNumber(value) {
1341
+ return typeof value === "number" && Number.isFinite(value) && value >= 0;
1342
+ }
1236
1343
  function stringifyJson(value) {
1237
1344
  const serialized = JSON.stringify(value);
1238
1345
  if (serialized === undefined) {
@@ -1281,9 +1388,8 @@
1281
1388
  }
1282
1389
  /** Triggers a SkyWalking performance report through an already registered client. */
1283
1390
  function setSkyWalkingPerformance(config = {}, customMetrics) {
1284
- const clientMonitor = getClientMonitor();
1285
- if (clientMonitor === undefined ||
1286
- typeof clientMonitor.setPerformance !== "function") {
1391
+ const clientMonitor = getPerformanceClientMonitor();
1392
+ if (clientMonitor === undefined) {
1287
1393
  warnOnce("set-performance-unavailable", "[LiveAvatar SkyWalking plugin] ClientMonitor.setPerformance is unavailable; register SkyWalking first");
1288
1394
  return;
1289
1395
  }
@@ -1308,6 +1414,16 @@
1308
1414
  }
1309
1415
  await Promise.resolve(clientMonitor.reportFrameErrors(config, error));
1310
1416
  }
1417
+ function normalizeReportingConfig(config) {
1418
+ return {
1419
+ sdkError: config?.sdkError ?? true,
1420
+ firstFrame: config?.firstFrame ?? true,
1421
+ avSync: config?.avSync ?? true,
1422
+ };
1423
+ }
1424
+ function hasEnabledReporting(config) {
1425
+ return config.sdkError || config.firstFrame || config.avSync;
1426
+ }
1311
1427
  function normalizeSkyWalkingConfig(config) {
1312
1428
  return {
1313
1429
  collector: config.collector ??
@@ -1355,6 +1471,13 @@
1355
1471
  ? candidate
1356
1472
  : undefined;
1357
1473
  }
1474
+ /** Finds the performance API without requiring the unrelated error API. */
1475
+ function getPerformanceClientMonitor() {
1476
+ const candidate = getGlobalClientMonitor();
1477
+ return typeof candidate?.setPerformance === "function"
1478
+ ? candidate
1479
+ : undefined;
1480
+ }
1358
1481
  function getGlobalClientMonitor() {
1359
1482
  if (typeof window === "undefined")
1360
1483
  return undefined;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@sanseng/liveavatar-js-sdk-skywalking",
3
- "version": "1.0.0",
4
- "description": "Bundled SkyWalking plugin for @sanseng/liveavatar-js-sdk A/V Sync diagnostics and Operational Log SDK errors",
3
+ "version": "1.0.1",
4
+ "description": "SkyWalking plugin for LiveAvatar A/V Sync alerts, first-frame performance, and SDK errors with type-selective reporting",
5
5
  "license": "MIT",
6
6
  "author": "sanseng",
7
7
  "main": "dist/index.cjs.js",
@@ -43,7 +43,7 @@
43
43
  "observability"
44
44
  ],
45
45
  "peerDependencies": {
46
- "@sanseng/liveavatar-js-sdk": "^2.2.0"
46
+ "@sanseng/liveavatar-js-sdk": "^2.4.0"
47
47
  },
48
48
  "devDependencies": {
49
49
  "@rollup/plugin-typescript": "^12.3.0",
@@ -53,6 +53,7 @@
53
53
  "rimraf": "^6.1.2",
54
54
  "rollup": "^4.55.1",
55
55
  "rollup-plugin-dts": "^6.3.0",
56
+ "tslib": "^2.8.1",
56
57
  "typescript": "^5.9.3",
57
58
  "vite": "^7.3.0",
58
59
  "vitest": "^4.1.7"