@chunkify/analytics 0.1.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/LICENSE +21 -0
- package/README.md +413 -0
- package/dist/core.d.ts +144 -0
- package/dist/core.d.ts.map +1 -0
- package/dist/core.js +660 -0
- package/dist/core.js.map +1 -0
- package/dist/datadog.d.ts +8 -0
- package/dist/datadog.d.ts.map +1 -0
- package/dist/datadog.js +15 -0
- package/dist/datadog.js.map +1 -0
- package/dist/event-types.d.ts +3 -0
- package/dist/event-types.d.ts.map +1 -0
- package/dist/event-types.js +5 -0
- package/dist/event-types.js.map +1 -0
- package/dist/hlsjs.d.ts +10 -0
- package/dist/hlsjs.d.ts.map +1 -0
- package/dist/hlsjs.js +185 -0
- package/dist/hlsjs.js.map +1 -0
- package/dist/http.d.ts +4 -0
- package/dist/http.d.ts.map +1 -0
- package/dist/http.js +43 -0
- package/dist/http.js.map +1 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +7 -0
- package/dist/index.js.map +1 -0
- package/dist/mapper.d.ts +11 -0
- package/dist/mapper.d.ts.map +1 -0
- package/dist/mapper.js +11 -0
- package/dist/mapper.js.map +1 -0
- package/dist/monitor.d.ts +6 -0
- package/dist/monitor.d.ts.map +1 -0
- package/dist/monitor.js +14 -0
- package/dist/monitor.js.map +1 -0
- package/dist/options.d.ts +4 -0
- package/dist/options.d.ts.map +1 -0
- package/dist/options.js +26 -0
- package/dist/options.js.map +1 -0
- package/dist/otlp.d.ts +4 -0
- package/dist/otlp.d.ts.map +1 -0
- package/dist/otlp.js +56 -0
- package/dist/otlp.js.map +1 -0
- package/dist/player.d.ts +8 -0
- package/dist/player.d.ts.map +1 -0
- package/dist/player.js +93 -0
- package/dist/player.js.map +1 -0
- package/dist/posthog.d.ts +8 -0
- package/dist/posthog.d.ts.map +1 -0
- package/dist/posthog.js +15 -0
- package/dist/posthog.js.map +1 -0
- package/dist/shaka.d.ts +26 -0
- package/dist/shaka.d.ts.map +1 -0
- package/dist/shaka.js +164 -0
- package/dist/shaka.js.map +1 -0
- package/dist/videojs.d.ts +11 -0
- package/dist/videojs.d.ts.map +1 -0
- package/dist/videojs.js +77 -0
- package/dist/videojs.js.map +1 -0
- package/package.json +75 -0
- package/src/core.ts +774 -0
- package/src/datadog.ts +24 -0
- package/src/event-types.ts +6 -0
- package/src/hlsjs.ts +192 -0
- package/src/http.ts +43 -0
- package/src/index.ts +11 -0
- package/src/mapper.ts +22 -0
- package/src/monitor.ts +15 -0
- package/src/options.ts +28 -0
- package/src/otlp.ts +70 -0
- package/src/player.ts +96 -0
- package/src/posthog.ts +24 -0
- package/src/shaka.ts +181 -0
- package/src/videojs.ts +89 -0
package/src/shaka.ts
ADDED
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
import { attachAnalyticsWithManagedSource, type AnalyticsConnector, type AnalyticsConnectorError,
|
|
2
|
+
type AnalyticsConnectorSnapshot, type AnalyticsOptions, type AnalyticsRendition,
|
|
3
|
+
type AnalyticsSession } from "./core.js";
|
|
4
|
+
import { assertAnalyticsOptions } from "./options.js";
|
|
5
|
+
|
|
6
|
+
interface ShakaMediaQuality {
|
|
7
|
+
contentType?: string;
|
|
8
|
+
width?: number | null;
|
|
9
|
+
height?: number | null;
|
|
10
|
+
codecs?: string | null;
|
|
11
|
+
bandwidth?: number | null;
|
|
12
|
+
frameRate?: number | null;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
interface ShakaEvent extends Event {
|
|
16
|
+
mediaQuality?: ShakaMediaQuality;
|
|
17
|
+
detail?: { severity?: number; category?: number; code?: number };
|
|
18
|
+
response?: { data?: ArrayBuffer | ArrayBufferView; timeMs?: number; fromCache?: boolean };
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** The public Shaka Player APIs used by analytics. */
|
|
22
|
+
export interface ShakaPlayer {
|
|
23
|
+
getMediaElement(): HTMLMediaElement | null;
|
|
24
|
+
getConfiguration(): { streaming: { observeQualityChanges: boolean } };
|
|
25
|
+
configure(config: { streaming: { observeQualityChanges: boolean } }): boolean;
|
|
26
|
+
addEventListener(type: string, listener: (event: Event) => void): void;
|
|
27
|
+
removeEventListener(type: string, listener: (event: Event) => void): void;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface ShakaAnalyticsConnector extends AnalyticsConnector {
|
|
31
|
+
reset(): void;
|
|
32
|
+
destroy(): void;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function positive(value: unknown): number | null {
|
|
36
|
+
return typeof value === "number" && Number.isFinite(value) && value > 0 ? value : null;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Observe Shaka's quality at the playhead, completed downloads, and critical errors. */
|
|
40
|
+
export function createShakaConnector(player: ShakaPlayer): ShakaAnalyticsConnector {
|
|
41
|
+
const errorListeners = new Set<(error: AnalyticsConnectorError) => void>();
|
|
42
|
+
const renditionListeners = new Set<(rendition: AnalyticsRendition | null) => void>();
|
|
43
|
+
let rendition: AnalyticsRendition | null = null;
|
|
44
|
+
let switches = 0;
|
|
45
|
+
let requests = 0;
|
|
46
|
+
let bytes = 0;
|
|
47
|
+
let durationMs = 0;
|
|
48
|
+
let bytesKnown = true;
|
|
49
|
+
let durationKnown = true;
|
|
50
|
+
let destroyed = false;
|
|
51
|
+
const version = (player.constructor as { version?: unknown }).version;
|
|
52
|
+
const playerVersion = typeof version === "string" ? version : null;
|
|
53
|
+
|
|
54
|
+
const onQuality = (event: Event) => {
|
|
55
|
+
const quality = (event as ShakaEvent).mediaQuality;
|
|
56
|
+
if (quality?.contentType !== "video") return;
|
|
57
|
+
const next: AnalyticsRendition = {
|
|
58
|
+
width: positive(quality.width), height: positive(quality.height),
|
|
59
|
+
codec: quality.codecs || null, bitrate_bps: positive(quality.bandwidth),
|
|
60
|
+
frame_rate: positive(quality.frameRate),
|
|
61
|
+
};
|
|
62
|
+
if (rendition && (next.width !== rendition.width || next.height !== rendition.height ||
|
|
63
|
+
next.codec !== rendition.codec || next.bitrate_bps !== rendition.bitrate_bps ||
|
|
64
|
+
next.frame_rate !== rendition.frame_rate)) switches++;
|
|
65
|
+
rendition = next;
|
|
66
|
+
for (const listener of renditionListeners) listener(rendition);
|
|
67
|
+
};
|
|
68
|
+
const onError = (event: Event) => {
|
|
69
|
+
const detail = (event as ShakaEvent).detail;
|
|
70
|
+
if (detail?.severity !== 2) return;
|
|
71
|
+
const category = detail.category === 1 ? "network"
|
|
72
|
+
: detail.category === 3 ? "decode" : "unknown";
|
|
73
|
+
for (const listener of errorListeners) listener({
|
|
74
|
+
category, code: Number.isFinite(detail.code) ? detail.code! : null, fatal: true,
|
|
75
|
+
});
|
|
76
|
+
};
|
|
77
|
+
const onDownload = (event: Event) => {
|
|
78
|
+
const response = (event as ShakaEvent).response;
|
|
79
|
+
if (response?.fromCache) return;
|
|
80
|
+
requests++;
|
|
81
|
+
const size = response?.data?.byteLength;
|
|
82
|
+
if (typeof size === "number" && Number.isFinite(size) && size >= 0) bytes += size;
|
|
83
|
+
else bytesKnown = false;
|
|
84
|
+
const duration = response?.timeMs;
|
|
85
|
+
if (typeof duration === "number" && Number.isFinite(duration) && duration >= 0) durationMs += duration;
|
|
86
|
+
else durationKnown = false;
|
|
87
|
+
};
|
|
88
|
+
player.addEventListener("mediaqualitychanged", onQuality);
|
|
89
|
+
player.addEventListener("error", onError);
|
|
90
|
+
player.addEventListener("downloadcompleted", onDownload);
|
|
91
|
+
|
|
92
|
+
return {
|
|
93
|
+
name: "shaka-player", version: playerVersion,
|
|
94
|
+
playerName: "shaka-player", playerVersion,
|
|
95
|
+
snapshot(): AnalyticsConnectorSnapshot {
|
|
96
|
+
return {
|
|
97
|
+
renditionWidth: rendition?.width ?? null,
|
|
98
|
+
renditionHeight: rendition?.height ?? null,
|
|
99
|
+
renditionCodec: rendition?.codec ?? null,
|
|
100
|
+
renditionBitrate: rendition?.bitrate_bps ?? null,
|
|
101
|
+
renditionSwitchCount: switches,
|
|
102
|
+
deliveryRequestCount: requests,
|
|
103
|
+
deliveryTransferredBytes: bytesKnown ? bytes : null,
|
|
104
|
+
deliveryDurationMs: durationKnown ? durationMs : null,
|
|
105
|
+
};
|
|
106
|
+
},
|
|
107
|
+
subscribeToErrors(listener) {
|
|
108
|
+
errorListeners.add(listener);
|
|
109
|
+
return () => { errorListeners.delete(listener); };
|
|
110
|
+
},
|
|
111
|
+
subscribeToRenditions(listener) {
|
|
112
|
+
renditionListeners.add(listener);
|
|
113
|
+
listener(rendition);
|
|
114
|
+
return () => { renditionListeners.delete(listener); };
|
|
115
|
+
},
|
|
116
|
+
reset() {
|
|
117
|
+
rendition = null;
|
|
118
|
+
switches = 0;
|
|
119
|
+
requests = 0;
|
|
120
|
+
bytes = 0;
|
|
121
|
+
durationMs = 0;
|
|
122
|
+
bytesKnown = true;
|
|
123
|
+
durationKnown = true;
|
|
124
|
+
for (const listener of renditionListeners) listener(null);
|
|
125
|
+
},
|
|
126
|
+
destroy() {
|
|
127
|
+
if (destroyed) return;
|
|
128
|
+
destroyed = true;
|
|
129
|
+
player.removeEventListener("mediaqualitychanged", onQuality);
|
|
130
|
+
player.removeEventListener("error", onError);
|
|
131
|
+
player.removeEventListener("downloadcompleted", onDownload);
|
|
132
|
+
errorListeners.clear();
|
|
133
|
+
renditionListeners.clear();
|
|
134
|
+
},
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/** Monitor an attached Shaka Player before loading and playing content. */
|
|
139
|
+
export function monitorShaka(player: ShakaPlayer, options: AnalyticsOptions):
|
|
140
|
+
Pick<AnalyticsSession, "startAttempt" | "sourceChanged" | "destroy"> {
|
|
141
|
+
assertAnalyticsOptions(options);
|
|
142
|
+
const media = player.getMediaElement();
|
|
143
|
+
const Media = media?.ownerDocument?.defaultView?.HTMLMediaElement;
|
|
144
|
+
if (!Media || !(media instanceof Media)) {
|
|
145
|
+
throw new TypeError("Shaka analytics requires a player attached to an HTML media element.");
|
|
146
|
+
}
|
|
147
|
+
// Shaka only emits quality changes at the playhead when this observer is enabled.
|
|
148
|
+
const observerWasEnabled = player.getConfiguration().streaming.observeQualityChanges;
|
|
149
|
+
if (!observerWasEnabled) {
|
|
150
|
+
player.configure({ streaming: { observeQualityChanges: true } });
|
|
151
|
+
}
|
|
152
|
+
const restoreObserver = !observerWasEnabled && player.getConfiguration().streaming.observeQualityChanges;
|
|
153
|
+
const session = attachAnalyticsWithManagedSource(media, options);
|
|
154
|
+
const connector = createShakaConnector(player);
|
|
155
|
+
session.setConnector(connector);
|
|
156
|
+
let destroyed = false;
|
|
157
|
+
const sourceChanged = () => {
|
|
158
|
+
if (destroyed) return;
|
|
159
|
+
session.sourceChanged();
|
|
160
|
+
connector.reset();
|
|
161
|
+
};
|
|
162
|
+
const onLoading = () => { if (!destroyed) session.startAttempt(); };
|
|
163
|
+
const onUnloading = () => sourceChanged();
|
|
164
|
+
player.addEventListener("loading", onLoading);
|
|
165
|
+
player.addEventListener("unloading", onUnloading);
|
|
166
|
+
return {
|
|
167
|
+
startAttempt() { if (!destroyed) session.startAttempt(); },
|
|
168
|
+
sourceChanged,
|
|
169
|
+
destroy() {
|
|
170
|
+
if (destroyed) return;
|
|
171
|
+
destroyed = true;
|
|
172
|
+
player.removeEventListener("loading", onLoading);
|
|
173
|
+
player.removeEventListener("unloading", onUnloading);
|
|
174
|
+
session.destroy();
|
|
175
|
+
connector.destroy();
|
|
176
|
+
if (restoreObserver) {
|
|
177
|
+
try { player.configure({ streaming: { observeQualityChanges: false } }); } catch {}
|
|
178
|
+
}
|
|
179
|
+
},
|
|
180
|
+
};
|
|
181
|
+
}
|
package/src/videojs.ts
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import type Hls from "hls.js";
|
|
2
|
+
import { attachAnalyticsWithManagedSource, type AnalyticsConnector, type AnalyticsConnectorSnapshot,
|
|
3
|
+
type AnalyticsOptions, type AnalyticsSession } from "./core.js";
|
|
4
|
+
import { createHlsJsConnector, type HlsJsAnalyticsConnector } from "./hlsjs.js";
|
|
5
|
+
import { assertAnalyticsOptions } from "./options.js";
|
|
6
|
+
|
|
7
|
+
/** Video.js 10 HTML media components expose the media API on the custom element. */
|
|
8
|
+
export type VideoJsMediaElement = HTMLElement & {
|
|
9
|
+
readonly src: string;
|
|
10
|
+
readonly engine?: Hls | object | null;
|
|
11
|
+
readonly adapter?: object;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
const videoJsMediaTags = new Set(["hls-video", "hlsjs-video", "native-hls-video"]);
|
|
15
|
+
|
|
16
|
+
function emptySnapshot(): AnalyticsConnectorSnapshot {
|
|
17
|
+
return {
|
|
18
|
+
renditionWidth: null, renditionHeight: null, renditionCodec: null, renditionBitrate: null,
|
|
19
|
+
renditionSwitchCount: null, deliveryRequestCount: null,
|
|
20
|
+
deliveryTransferredBytes: null, deliveryDurationMs: null,
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function videoJsConnector(engine: Hls | null): AnalyticsConnector & { destroy(): void } {
|
|
25
|
+
if (engine) {
|
|
26
|
+
const hls: HlsJsAnalyticsConnector = createHlsJsConnector(engine);
|
|
27
|
+
return { ...hls, name: "video.js/hls.js", playerName: "video.js", playerVersion: null };
|
|
28
|
+
}
|
|
29
|
+
return {
|
|
30
|
+
name: "video.js", version: null, playerName: "video.js", playerVersion: null,
|
|
31
|
+
snapshot: emptySnapshot,
|
|
32
|
+
subscribeToErrors: () => () => {},
|
|
33
|
+
destroy() {},
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** Monitor a Video.js 10 HTML media component, including its hls.js engine when active. */
|
|
38
|
+
export function monitorVideoJs(media: VideoJsMediaElement, options: AnalyticsOptions):
|
|
39
|
+
Pick<AnalyticsSession, "startAttempt" | "sourceChanged" | "destroy"> {
|
|
40
|
+
assertAnalyticsOptions(options);
|
|
41
|
+
if (!media || !videoJsMediaTags.has(media.localName)
|
|
42
|
+
|| typeof (media as { play?: unknown }).play !== "function"
|
|
43
|
+
|| typeof media.adapter !== "object" || media.adapter === null) {
|
|
44
|
+
throw new TypeError("Video.js 10 analytics requires a registered hls-video, hlsjs-video, or native-hls-video element from @videojs/html.");
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const session = attachAnalyticsWithManagedSource(media as unknown as HTMLMediaElement, options);
|
|
48
|
+
let currentSource = media.src;
|
|
49
|
+
let currentEngine: Hls | null = null;
|
|
50
|
+
let connector: (AnalyticsConnector & { destroy(): void }) | null = null;
|
|
51
|
+
let destroyed = false;
|
|
52
|
+
|
|
53
|
+
const syncConnector = () => {
|
|
54
|
+
const engine = media.localName === "hlsjs-video" ? (media.engine as Hls | null | undefined) ?? null : null;
|
|
55
|
+
if (connector && engine === currentEngine) return;
|
|
56
|
+
connector?.destroy();
|
|
57
|
+
currentEngine = engine;
|
|
58
|
+
connector = videoJsConnector(engine);
|
|
59
|
+
session.setConnector(connector);
|
|
60
|
+
};
|
|
61
|
+
const onSourceChange = () => {
|
|
62
|
+
if (media.src === currentSource) return;
|
|
63
|
+
currentSource = media.src;
|
|
64
|
+
session.sourceChanged();
|
|
65
|
+
connector?.destroy();
|
|
66
|
+
connector = null;
|
|
67
|
+
currentEngine = null;
|
|
68
|
+
session.setConnector(null);
|
|
69
|
+
};
|
|
70
|
+
const onLoadStart = () => syncConnector();
|
|
71
|
+
|
|
72
|
+
media.addEventListener("sourcechange", onSourceChange);
|
|
73
|
+
media.addEventListener("loadstart", onLoadStart);
|
|
74
|
+
syncConnector();
|
|
75
|
+
|
|
76
|
+
return {
|
|
77
|
+
startAttempt() { if (!destroyed) session.startAttempt(); },
|
|
78
|
+
sourceChanged() { if (!destroyed) session.sourceChanged(); },
|
|
79
|
+
destroy() {
|
|
80
|
+
if (destroyed) return;
|
|
81
|
+
destroyed = true;
|
|
82
|
+
media.removeEventListener("sourcechange", onSourceChange);
|
|
83
|
+
media.removeEventListener("loadstart", onLoadStart);
|
|
84
|
+
session.destroy();
|
|
85
|
+
connector?.destroy();
|
|
86
|
+
connector = null;
|
|
87
|
+
},
|
|
88
|
+
};
|
|
89
|
+
}
|