@ngxsp/telemetry-core 0.0.1-next.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/index.cjs ADDED
@@ -0,0 +1,95 @@
1
+ 'use strict';
2
+
3
+ // src/index.ts
4
+ function randomId() {
5
+ return Math.random().toString(36).slice(2) + "-" + Date.now().toString(36);
6
+ }
7
+ function clamp(n, min, max) {
8
+ return Math.max(min, Math.min(max, n));
9
+ }
10
+ var TelemetryCollector = class {
11
+ meta;
12
+ samples = [];
13
+ constructor(meta) {
14
+ this.meta = {
15
+ sessionId: meta?.sessionId ?? randomId(),
16
+ startedAt: meta?.startedAt ?? Date.now(),
17
+ profile: meta?.profile,
18
+ app: meta?.app,
19
+ version: meta?.version,
20
+ contentId: meta?.contentId
21
+ };
22
+ }
23
+ setMeta(meta) {
24
+ this.meta = { ...this.meta, ...meta };
25
+ }
26
+ reset(meta) {
27
+ this.samples = [];
28
+ this.meta = {
29
+ sessionId: meta?.sessionId ?? randomId(),
30
+ startedAt: meta?.startedAt ?? Date.now(),
31
+ profile: meta?.profile ?? this.meta.profile,
32
+ app: meta?.app ?? this.meta.app,
33
+ version: meta?.version ?? this.meta.version,
34
+ contentId: meta?.contentId ?? this.meta.contentId
35
+ };
36
+ }
37
+ add(sample) {
38
+ this.samples.push(sample);
39
+ }
40
+ export() {
41
+ return { startedAt: this.meta.startedAt, samples: [...this.samples] };
42
+ }
43
+ exportV2() {
44
+ const samples = [...this.samples];
45
+ const summary = this.computeSummary(samples);
46
+ return { meta: { ...this.meta }, samples, summary };
47
+ }
48
+ toJSON(pretty = true) {
49
+ const snap = this.exportV2();
50
+ return JSON.stringify(snap, null, pretty ? 2 : 0);
51
+ }
52
+ toNDJSON() {
53
+ const snap = this.exportV2();
54
+ const lines = [];
55
+ lines.push(JSON.stringify({ type: "meta", ...snap.meta }));
56
+ for (const s of snap.samples) lines.push(JSON.stringify({ type: "sample", ...s }));
57
+ lines.push(JSON.stringify({ type: "summary", ...snap.summary }));
58
+ return lines.join("\n") + "\n";
59
+ }
60
+ computeSummary(samples) {
61
+ let startupMs;
62
+ let firstFrameMs;
63
+ let rebufferCount = 0;
64
+ let rebufferMs = 0;
65
+ let fatalCount = 0;
66
+ let nonFatalCount = 0;
67
+ for (const s of samples) {
68
+ if (s.event === "qoe:startup" && typeof s.data?.["startupMs"] === "number") startupMs = s.data["startupMs"];
69
+ if (s.event === "qoe:first_frame" && typeof s.data?.["firstFrameMs"] === "number") firstFrameMs = s.data["firstFrameMs"];
70
+ if (s.event === "qoe:rebuffer_end") {
71
+ rebufferCount += 1;
72
+ if (typeof s.data?.["deltaMs"] === "number") rebufferMs += s.data["deltaMs"];
73
+ }
74
+ if (s.event === "player:fatal_error") fatalCount += 1;
75
+ if (s.event === "player:non_fatal_error") nonFatalCount += 1;
76
+ }
77
+ const startupPenalty = startupMs != null ? clamp(startupMs / 200, 0, 35) : 0;
78
+ const rebufferPenalty = clamp(rebufferMs / 1e3 * 6, 0, 50);
79
+ const errorPenalty = clamp(fatalCount * 30 + nonFatalCount * 5, 0, 60);
80
+ const score = clamp(Math.round(100 - startupPenalty - rebufferPenalty - errorPenalty), 0, 100);
81
+ return {
82
+ startupMs,
83
+ firstFrameMs,
84
+ rebufferCount,
85
+ rebufferMs,
86
+ fatalCount,
87
+ nonFatalCount,
88
+ score
89
+ };
90
+ }
91
+ };
92
+
93
+ exports.TelemetryCollector = TelemetryCollector;
94
+ //# sourceMappingURL=index.cjs.map
95
+ //# sourceMappingURL=index.cjs.map
package/index.cjs.map ADDED
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/index.ts"],"names":[],"mappings":"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","file":"index.cjs","sourcesContent":["export type TelemetryLevel = 'debug' | 'info' | 'warn' | 'error';\n\n/**\n * Canonical telemetry sample used across the workspace.\n *\n * This intentionally mirrors what @ngxsp/core emits through the telemetry port.\n */\nexport interface QoEMetricSample {\n ts: number;\n level: TelemetryLevel;\n event: string;\n contentId?: string;\n engineId?: string;\n data?: Record<string, unknown>;\n}\n\nexport interface TelemetrySessionMeta {\n sessionId: string;\n startedAt: number;\n profile?: string;\n app?: string;\n version?: string;\n contentId?: string;\n}\n\nexport interface TelemetrySummary {\n startupMs?: number;\n firstFrameMs?: number;\n rebufferCount: number;\n rebufferMs: number;\n fatalCount: number;\n nonFatalCount: number;\n /** A pragmatic 0..100 score. Higher is better. */\n score: number;\n}\n\nexport interface TelemetrySessionV2 {\n meta: TelemetrySessionMeta;\n samples: QoEMetricSample[];\n summary: TelemetrySummary;\n}\n\n/**\n * Back-compat shape.\n *\n * NOTE: this is kept to avoid breaking existing `ports.telemetry.export()` contracts.\n */\nexport interface TelemetrySession {\n startedAt: number;\n samples: QoEMetricSample[];\n}\n\nfunction randomId(): string {\n return Math.random().toString(36).slice(2) + '-' + Date.now().toString(36);\n}\n\nfunction clamp(n: number, min: number, max: number): number {\n return Math.max(min, Math.min(max, n));\n}\n\n/**\n * In-memory collector + lightweight scoring.\n *\n * - `export()` returns the legacy shape.\n * - `exportV2()` returns meta + computed summary.\n */\nexport class TelemetryCollector {\n private meta: TelemetrySessionMeta;\n private samples: QoEMetricSample[] = [];\n\n constructor(\n meta?: Partial<Omit<TelemetrySessionMeta, 'sessionId' | 'startedAt'>> & {\n sessionId?: string;\n startedAt?: number;\n }\n ) {\n this.meta = {\n sessionId: meta?.sessionId ?? randomId(),\n startedAt: meta?.startedAt ?? Date.now(),\n profile: meta?.profile,\n app: meta?.app,\n version: meta?.version,\n contentId: meta?.contentId\n };\n }\n\n setMeta(meta: Partial<TelemetrySessionMeta>): void {\n this.meta = { ...this.meta, ...meta };\n }\n\n reset(meta?: Partial<TelemetrySessionMeta>): void {\n this.samples = [];\n this.meta = {\n sessionId: meta?.sessionId ?? randomId(),\n startedAt: meta?.startedAt ?? Date.now(),\n profile: meta?.profile ?? this.meta.profile,\n app: meta?.app ?? this.meta.app,\n version: meta?.version ?? this.meta.version,\n contentId: meta?.contentId ?? this.meta.contentId\n };\n }\n\n add(sample: QoEMetricSample): void {\n this.samples.push(sample);\n }\n\n export(): TelemetrySession {\n return { startedAt: this.meta.startedAt, samples: [...this.samples] };\n }\n\n exportV2(): TelemetrySessionV2 {\n const samples = [...this.samples];\n const summary = this.computeSummary(samples);\n return { meta: { ...this.meta }, samples, summary };\n }\n\n toJSON(pretty = true): string {\n const snap = this.exportV2();\n return JSON.stringify(snap, null, pretty ? 2 : 0);\n }\n\n toNDJSON(): string {\n const snap = this.exportV2();\n const lines: string[] = [];\n lines.push(JSON.stringify({ type: 'meta', ...snap.meta }));\n for (const s of snap.samples) lines.push(JSON.stringify({ type: 'sample', ...s }));\n lines.push(JSON.stringify({ type: 'summary', ...snap.summary }));\n return lines.join('\\n') + '\\n';\n }\n\n private computeSummary(samples: QoEMetricSample[]): TelemetrySummary {\n let startupMs: number | undefined;\n let firstFrameMs: number | undefined;\n let rebufferCount = 0;\n let rebufferMs = 0;\n let fatalCount = 0;\n let nonFatalCount = 0;\n\n for (const s of samples) {\n if (s.event === 'qoe:startup' && typeof s.data?.['startupMs'] === 'number') startupMs = s.data['startupMs'] as number;\n if (s.event === 'qoe:first_frame' && typeof s.data?.['firstFrameMs'] === 'number') firstFrameMs = s.data['firstFrameMs'] as number;\n if (s.event === 'qoe:rebuffer_end') {\n rebufferCount += 1;\n if (typeof s.data?.['deltaMs'] === 'number') rebufferMs += s.data['deltaMs'] as number;\n }\n if (s.event === 'player:fatal_error') fatalCount += 1;\n if (s.event === 'player:non_fatal_error') nonFatalCount += 1;\n }\n\n // Simple heuristic: penalize startup and rebuffer, cap to [0..100].\n const startupPenalty = startupMs != null ? clamp(startupMs / 200, 0, 35) : 0; // 7s => 35\n const rebufferPenalty = clamp((rebufferMs / 1000) * 6, 0, 50); // 8s => 48\n const errorPenalty = clamp((fatalCount * 30) + (nonFatalCount * 5), 0, 60);\n const score = clamp(Math.round(100 - startupPenalty - rebufferPenalty - errorPenalty), 0, 100);\n\n return {\n startupMs,\n firstFrameMs,\n rebufferCount,\n rebufferMs,\n fatalCount,\n nonFatalCount,\n score\n };\n }\n}\n"]}
package/index.d.cts ADDED
@@ -0,0 +1,70 @@
1
+ type TelemetryLevel = 'debug' | 'info' | 'warn' | 'error';
2
+ /**
3
+ * Canonical telemetry sample used across the workspace.
4
+ *
5
+ * This intentionally mirrors what @ngxsp/core emits through the telemetry port.
6
+ */
7
+ interface QoEMetricSample {
8
+ ts: number;
9
+ level: TelemetryLevel;
10
+ event: string;
11
+ contentId?: string;
12
+ engineId?: string;
13
+ data?: Record<string, unknown>;
14
+ }
15
+ interface TelemetrySessionMeta {
16
+ sessionId: string;
17
+ startedAt: number;
18
+ profile?: string;
19
+ app?: string;
20
+ version?: string;
21
+ contentId?: string;
22
+ }
23
+ interface TelemetrySummary {
24
+ startupMs?: number;
25
+ firstFrameMs?: number;
26
+ rebufferCount: number;
27
+ rebufferMs: number;
28
+ fatalCount: number;
29
+ nonFatalCount: number;
30
+ /** A pragmatic 0..100 score. Higher is better. */
31
+ score: number;
32
+ }
33
+ interface TelemetrySessionV2 {
34
+ meta: TelemetrySessionMeta;
35
+ samples: QoEMetricSample[];
36
+ summary: TelemetrySummary;
37
+ }
38
+ /**
39
+ * Back-compat shape.
40
+ *
41
+ * NOTE: this is kept to avoid breaking existing `ports.telemetry.export()` contracts.
42
+ */
43
+ interface TelemetrySession {
44
+ startedAt: number;
45
+ samples: QoEMetricSample[];
46
+ }
47
+ /**
48
+ * In-memory collector + lightweight scoring.
49
+ *
50
+ * - `export()` returns the legacy shape.
51
+ * - `exportV2()` returns meta + computed summary.
52
+ */
53
+ declare class TelemetryCollector {
54
+ private meta;
55
+ private samples;
56
+ constructor(meta?: Partial<Omit<TelemetrySessionMeta, 'sessionId' | 'startedAt'>> & {
57
+ sessionId?: string;
58
+ startedAt?: number;
59
+ });
60
+ setMeta(meta: Partial<TelemetrySessionMeta>): void;
61
+ reset(meta?: Partial<TelemetrySessionMeta>): void;
62
+ add(sample: QoEMetricSample): void;
63
+ export(): TelemetrySession;
64
+ exportV2(): TelemetrySessionV2;
65
+ toJSON(pretty?: boolean): string;
66
+ toNDJSON(): string;
67
+ private computeSummary;
68
+ }
69
+
70
+ export { type QoEMetricSample, TelemetryCollector, type TelemetryLevel, type TelemetrySession, type TelemetrySessionMeta, type TelemetrySessionV2, type TelemetrySummary };
package/index.d.ts ADDED
@@ -0,0 +1,70 @@
1
+ type TelemetryLevel = 'debug' | 'info' | 'warn' | 'error';
2
+ /**
3
+ * Canonical telemetry sample used across the workspace.
4
+ *
5
+ * This intentionally mirrors what @ngxsp/core emits through the telemetry port.
6
+ */
7
+ interface QoEMetricSample {
8
+ ts: number;
9
+ level: TelemetryLevel;
10
+ event: string;
11
+ contentId?: string;
12
+ engineId?: string;
13
+ data?: Record<string, unknown>;
14
+ }
15
+ interface TelemetrySessionMeta {
16
+ sessionId: string;
17
+ startedAt: number;
18
+ profile?: string;
19
+ app?: string;
20
+ version?: string;
21
+ contentId?: string;
22
+ }
23
+ interface TelemetrySummary {
24
+ startupMs?: number;
25
+ firstFrameMs?: number;
26
+ rebufferCount: number;
27
+ rebufferMs: number;
28
+ fatalCount: number;
29
+ nonFatalCount: number;
30
+ /** A pragmatic 0..100 score. Higher is better. */
31
+ score: number;
32
+ }
33
+ interface TelemetrySessionV2 {
34
+ meta: TelemetrySessionMeta;
35
+ samples: QoEMetricSample[];
36
+ summary: TelemetrySummary;
37
+ }
38
+ /**
39
+ * Back-compat shape.
40
+ *
41
+ * NOTE: this is kept to avoid breaking existing `ports.telemetry.export()` contracts.
42
+ */
43
+ interface TelemetrySession {
44
+ startedAt: number;
45
+ samples: QoEMetricSample[];
46
+ }
47
+ /**
48
+ * In-memory collector + lightweight scoring.
49
+ *
50
+ * - `export()` returns the legacy shape.
51
+ * - `exportV2()` returns meta + computed summary.
52
+ */
53
+ declare class TelemetryCollector {
54
+ private meta;
55
+ private samples;
56
+ constructor(meta?: Partial<Omit<TelemetrySessionMeta, 'sessionId' | 'startedAt'>> & {
57
+ sessionId?: string;
58
+ startedAt?: number;
59
+ });
60
+ setMeta(meta: Partial<TelemetrySessionMeta>): void;
61
+ reset(meta?: Partial<TelemetrySessionMeta>): void;
62
+ add(sample: QoEMetricSample): void;
63
+ export(): TelemetrySession;
64
+ exportV2(): TelemetrySessionV2;
65
+ toJSON(pretty?: boolean): string;
66
+ toNDJSON(): string;
67
+ private computeSummary;
68
+ }
69
+
70
+ export { type QoEMetricSample, TelemetryCollector, type TelemetryLevel, type TelemetrySession, type TelemetrySessionMeta, type TelemetrySessionV2, type TelemetrySummary };
package/index.js ADDED
@@ -0,0 +1,93 @@
1
+ // src/index.ts
2
+ function randomId() {
3
+ return Math.random().toString(36).slice(2) + "-" + Date.now().toString(36);
4
+ }
5
+ function clamp(n, min, max) {
6
+ return Math.max(min, Math.min(max, n));
7
+ }
8
+ var TelemetryCollector = class {
9
+ meta;
10
+ samples = [];
11
+ constructor(meta) {
12
+ this.meta = {
13
+ sessionId: meta?.sessionId ?? randomId(),
14
+ startedAt: meta?.startedAt ?? Date.now(),
15
+ profile: meta?.profile,
16
+ app: meta?.app,
17
+ version: meta?.version,
18
+ contentId: meta?.contentId
19
+ };
20
+ }
21
+ setMeta(meta) {
22
+ this.meta = { ...this.meta, ...meta };
23
+ }
24
+ reset(meta) {
25
+ this.samples = [];
26
+ this.meta = {
27
+ sessionId: meta?.sessionId ?? randomId(),
28
+ startedAt: meta?.startedAt ?? Date.now(),
29
+ profile: meta?.profile ?? this.meta.profile,
30
+ app: meta?.app ?? this.meta.app,
31
+ version: meta?.version ?? this.meta.version,
32
+ contentId: meta?.contentId ?? this.meta.contentId
33
+ };
34
+ }
35
+ add(sample) {
36
+ this.samples.push(sample);
37
+ }
38
+ export() {
39
+ return { startedAt: this.meta.startedAt, samples: [...this.samples] };
40
+ }
41
+ exportV2() {
42
+ const samples = [...this.samples];
43
+ const summary = this.computeSummary(samples);
44
+ return { meta: { ...this.meta }, samples, summary };
45
+ }
46
+ toJSON(pretty = true) {
47
+ const snap = this.exportV2();
48
+ return JSON.stringify(snap, null, pretty ? 2 : 0);
49
+ }
50
+ toNDJSON() {
51
+ const snap = this.exportV2();
52
+ const lines = [];
53
+ lines.push(JSON.stringify({ type: "meta", ...snap.meta }));
54
+ for (const s of snap.samples) lines.push(JSON.stringify({ type: "sample", ...s }));
55
+ lines.push(JSON.stringify({ type: "summary", ...snap.summary }));
56
+ return lines.join("\n") + "\n";
57
+ }
58
+ computeSummary(samples) {
59
+ let startupMs;
60
+ let firstFrameMs;
61
+ let rebufferCount = 0;
62
+ let rebufferMs = 0;
63
+ let fatalCount = 0;
64
+ let nonFatalCount = 0;
65
+ for (const s of samples) {
66
+ if (s.event === "qoe:startup" && typeof s.data?.["startupMs"] === "number") startupMs = s.data["startupMs"];
67
+ if (s.event === "qoe:first_frame" && typeof s.data?.["firstFrameMs"] === "number") firstFrameMs = s.data["firstFrameMs"];
68
+ if (s.event === "qoe:rebuffer_end") {
69
+ rebufferCount += 1;
70
+ if (typeof s.data?.["deltaMs"] === "number") rebufferMs += s.data["deltaMs"];
71
+ }
72
+ if (s.event === "player:fatal_error") fatalCount += 1;
73
+ if (s.event === "player:non_fatal_error") nonFatalCount += 1;
74
+ }
75
+ const startupPenalty = startupMs != null ? clamp(startupMs / 200, 0, 35) : 0;
76
+ const rebufferPenalty = clamp(rebufferMs / 1e3 * 6, 0, 50);
77
+ const errorPenalty = clamp(fatalCount * 30 + nonFatalCount * 5, 0, 60);
78
+ const score = clamp(Math.round(100 - startupPenalty - rebufferPenalty - errorPenalty), 0, 100);
79
+ return {
80
+ startupMs,
81
+ firstFrameMs,
82
+ rebufferCount,
83
+ rebufferMs,
84
+ fatalCount,
85
+ nonFatalCount,
86
+ score
87
+ };
88
+ }
89
+ };
90
+
91
+ export { TelemetryCollector };
92
+ //# sourceMappingURL=index.js.map
93
+ //# sourceMappingURL=index.js.map
package/index.js.map ADDED
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/index.ts"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["export type TelemetryLevel = 'debug' | 'info' | 'warn' | 'error';\n\n/**\n * Canonical telemetry sample used across the workspace.\n *\n * This intentionally mirrors what @ngxsp/core emits through the telemetry port.\n */\nexport interface QoEMetricSample {\n ts: number;\n level: TelemetryLevel;\n event: string;\n contentId?: string;\n engineId?: string;\n data?: Record<string, unknown>;\n}\n\nexport interface TelemetrySessionMeta {\n sessionId: string;\n startedAt: number;\n profile?: string;\n app?: string;\n version?: string;\n contentId?: string;\n}\n\nexport interface TelemetrySummary {\n startupMs?: number;\n firstFrameMs?: number;\n rebufferCount: number;\n rebufferMs: number;\n fatalCount: number;\n nonFatalCount: number;\n /** A pragmatic 0..100 score. Higher is better. */\n score: number;\n}\n\nexport interface TelemetrySessionV2 {\n meta: TelemetrySessionMeta;\n samples: QoEMetricSample[];\n summary: TelemetrySummary;\n}\n\n/**\n * Back-compat shape.\n *\n * NOTE: this is kept to avoid breaking existing `ports.telemetry.export()` contracts.\n */\nexport interface TelemetrySession {\n startedAt: number;\n samples: QoEMetricSample[];\n}\n\nfunction randomId(): string {\n return Math.random().toString(36).slice(2) + '-' + Date.now().toString(36);\n}\n\nfunction clamp(n: number, min: number, max: number): number {\n return Math.max(min, Math.min(max, n));\n}\n\n/**\n * In-memory collector + lightweight scoring.\n *\n * - `export()` returns the legacy shape.\n * - `exportV2()` returns meta + computed summary.\n */\nexport class TelemetryCollector {\n private meta: TelemetrySessionMeta;\n private samples: QoEMetricSample[] = [];\n\n constructor(\n meta?: Partial<Omit<TelemetrySessionMeta, 'sessionId' | 'startedAt'>> & {\n sessionId?: string;\n startedAt?: number;\n }\n ) {\n this.meta = {\n sessionId: meta?.sessionId ?? randomId(),\n startedAt: meta?.startedAt ?? Date.now(),\n profile: meta?.profile,\n app: meta?.app,\n version: meta?.version,\n contentId: meta?.contentId\n };\n }\n\n setMeta(meta: Partial<TelemetrySessionMeta>): void {\n this.meta = { ...this.meta, ...meta };\n }\n\n reset(meta?: Partial<TelemetrySessionMeta>): void {\n this.samples = [];\n this.meta = {\n sessionId: meta?.sessionId ?? randomId(),\n startedAt: meta?.startedAt ?? Date.now(),\n profile: meta?.profile ?? this.meta.profile,\n app: meta?.app ?? this.meta.app,\n version: meta?.version ?? this.meta.version,\n contentId: meta?.contentId ?? this.meta.contentId\n };\n }\n\n add(sample: QoEMetricSample): void {\n this.samples.push(sample);\n }\n\n export(): TelemetrySession {\n return { startedAt: this.meta.startedAt, samples: [...this.samples] };\n }\n\n exportV2(): TelemetrySessionV2 {\n const samples = [...this.samples];\n const summary = this.computeSummary(samples);\n return { meta: { ...this.meta }, samples, summary };\n }\n\n toJSON(pretty = true): string {\n const snap = this.exportV2();\n return JSON.stringify(snap, null, pretty ? 2 : 0);\n }\n\n toNDJSON(): string {\n const snap = this.exportV2();\n const lines: string[] = [];\n lines.push(JSON.stringify({ type: 'meta', ...snap.meta }));\n for (const s of snap.samples) lines.push(JSON.stringify({ type: 'sample', ...s }));\n lines.push(JSON.stringify({ type: 'summary', ...snap.summary }));\n return lines.join('\\n') + '\\n';\n }\n\n private computeSummary(samples: QoEMetricSample[]): TelemetrySummary {\n let startupMs: number | undefined;\n let firstFrameMs: number | undefined;\n let rebufferCount = 0;\n let rebufferMs = 0;\n let fatalCount = 0;\n let nonFatalCount = 0;\n\n for (const s of samples) {\n if (s.event === 'qoe:startup' && typeof s.data?.['startupMs'] === 'number') startupMs = s.data['startupMs'] as number;\n if (s.event === 'qoe:first_frame' && typeof s.data?.['firstFrameMs'] === 'number') firstFrameMs = s.data['firstFrameMs'] as number;\n if (s.event === 'qoe:rebuffer_end') {\n rebufferCount += 1;\n if (typeof s.data?.['deltaMs'] === 'number') rebufferMs += s.data['deltaMs'] as number;\n }\n if (s.event === 'player:fatal_error') fatalCount += 1;\n if (s.event === 'player:non_fatal_error') nonFatalCount += 1;\n }\n\n // Simple heuristic: penalize startup and rebuffer, cap to [0..100].\n const startupPenalty = startupMs != null ? clamp(startupMs / 200, 0, 35) : 0; // 7s => 35\n const rebufferPenalty = clamp((rebufferMs / 1000) * 6, 0, 50); // 8s => 48\n const errorPenalty = clamp((fatalCount * 30) + (nonFatalCount * 5), 0, 60);\n const score = clamp(Math.round(100 - startupPenalty - rebufferPenalty - errorPenalty), 0, 100);\n\n return {\n startupMs,\n firstFrameMs,\n rebufferCount,\n rebufferMs,\n fatalCount,\n nonFatalCount,\n score\n };\n }\n}\n"]}
package/package.json ADDED
@@ -0,0 +1,15 @@
1
+ {
2
+ "name": "@ngxsp/telemetry-core",
3
+ "version": "0.0.1-next.0",
4
+ "type": "module",
5
+ "main": "./dist/index.cjs",
6
+ "module": "./dist/index.js",
7
+ "types": "./dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "import": "./dist/index.js",
12
+ "require": "./dist/index.cjs"
13
+ }
14
+ }
15
+ }