@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.
Files changed (73) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +413 -0
  3. package/dist/core.d.ts +144 -0
  4. package/dist/core.d.ts.map +1 -0
  5. package/dist/core.js +660 -0
  6. package/dist/core.js.map +1 -0
  7. package/dist/datadog.d.ts +8 -0
  8. package/dist/datadog.d.ts.map +1 -0
  9. package/dist/datadog.js +15 -0
  10. package/dist/datadog.js.map +1 -0
  11. package/dist/event-types.d.ts +3 -0
  12. package/dist/event-types.d.ts.map +1 -0
  13. package/dist/event-types.js +5 -0
  14. package/dist/event-types.js.map +1 -0
  15. package/dist/hlsjs.d.ts +10 -0
  16. package/dist/hlsjs.d.ts.map +1 -0
  17. package/dist/hlsjs.js +185 -0
  18. package/dist/hlsjs.js.map +1 -0
  19. package/dist/http.d.ts +4 -0
  20. package/dist/http.d.ts.map +1 -0
  21. package/dist/http.js +43 -0
  22. package/dist/http.js.map +1 -0
  23. package/dist/index.d.ts +8 -0
  24. package/dist/index.d.ts.map +1 -0
  25. package/dist/index.js +7 -0
  26. package/dist/index.js.map +1 -0
  27. package/dist/mapper.d.ts +11 -0
  28. package/dist/mapper.d.ts.map +1 -0
  29. package/dist/mapper.js +11 -0
  30. package/dist/mapper.js.map +1 -0
  31. package/dist/monitor.d.ts +6 -0
  32. package/dist/monitor.d.ts.map +1 -0
  33. package/dist/monitor.js +14 -0
  34. package/dist/monitor.js.map +1 -0
  35. package/dist/options.d.ts +4 -0
  36. package/dist/options.d.ts.map +1 -0
  37. package/dist/options.js +26 -0
  38. package/dist/options.js.map +1 -0
  39. package/dist/otlp.d.ts +4 -0
  40. package/dist/otlp.d.ts.map +1 -0
  41. package/dist/otlp.js +56 -0
  42. package/dist/otlp.js.map +1 -0
  43. package/dist/player.d.ts +8 -0
  44. package/dist/player.d.ts.map +1 -0
  45. package/dist/player.js +93 -0
  46. package/dist/player.js.map +1 -0
  47. package/dist/posthog.d.ts +8 -0
  48. package/dist/posthog.d.ts.map +1 -0
  49. package/dist/posthog.js +15 -0
  50. package/dist/posthog.js.map +1 -0
  51. package/dist/shaka.d.ts +26 -0
  52. package/dist/shaka.d.ts.map +1 -0
  53. package/dist/shaka.js +164 -0
  54. package/dist/shaka.js.map +1 -0
  55. package/dist/videojs.d.ts +11 -0
  56. package/dist/videojs.d.ts.map +1 -0
  57. package/dist/videojs.js +77 -0
  58. package/dist/videojs.js.map +1 -0
  59. package/package.json +75 -0
  60. package/src/core.ts +774 -0
  61. package/src/datadog.ts +24 -0
  62. package/src/event-types.ts +6 -0
  63. package/src/hlsjs.ts +192 -0
  64. package/src/http.ts +43 -0
  65. package/src/index.ts +11 -0
  66. package/src/mapper.ts +22 -0
  67. package/src/monitor.ts +15 -0
  68. package/src/options.ts +28 -0
  69. package/src/otlp.ts +70 -0
  70. package/src/player.ts +96 -0
  71. package/src/posthog.ts +24 -0
  72. package/src/shaka.ts +181 -0
  73. package/src/videojs.ts +89 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Chunkify contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,413 @@
1
+ # @chunkify/analytics
2
+
3
+ Standalone playback analytics for Chunkify Player, a native `<video>`, and supported third-party players. It has no runtime dependency on `@chunkify/player` and does not send data to Chunkify. Each event goes to the destinations supplied by the application.
4
+
5
+ ```sh
6
+ npm install @chunkify/analytics
7
+ ```
8
+
9
+ ## Monitor Chunkify Player or a video element
10
+
11
+ Add the player to your page:
12
+
13
+ ```html
14
+ <chunkify-player id="player" controls src="https://example.com/video.m3u8"></chunkify-player>
15
+ ```
16
+
17
+ Replace the example URL with your playback URL. In your application JavaScript, register the component and start monitoring it:
18
+
19
+ ```js
20
+ import "@chunkify/player";
21
+ import { monitor } from "@chunkify/analytics";
22
+
23
+ const player = document.querySelector("#player");
24
+ const session = monitor(player, {
25
+ videoId: "video_123",
26
+ destinations: [{ send: (event) => console.log(event) }],
27
+ });
28
+ ```
29
+
30
+ Call `session.destroy()` when the application removes the player or stops monitoring it.
31
+
32
+ The same function also accepts a native video element without importing `@chunkify/player`:
33
+
34
+ ```html
35
+ <video id="video" controls src="https://example.com/video.mp4"></video>
36
+ ```
37
+
38
+ ```js
39
+ import { monitor } from "@chunkify/analytics";
40
+
41
+ const video = document.querySelector("#video");
42
+ const session = monitor(video, {
43
+ videoId: "video_123",
44
+ destinations: [{ send: (event) => console.log(event) }],
45
+ });
46
+ ```
47
+
48
+ Call `session.startAttempt()` before an application-initiated `video.play()` to count attempts that reject without a play event. Call `session.destroy()` when removing the video or disabling analytics.
49
+
50
+ `monitor()` selects the direct video collector for `<video>` or the Chunkify adapter for `<chunkify-player>`. The adapter follows the player's active media element when switching between ordinary video and HLS. Other player types are not supported by this function. The lower-level `attachAnalytics()` and `attachPlayerAnalytics()` functions remain available for explicit use.
51
+
52
+ When Chunkify Player uses hls.js, the adapter also records the selected rendition, rendition switches, playing time by rendition, HLS manifest and segment request totals, transferred bytes, aggregate throughput, and fatal hls.js errors. These measurements are unavailable when playback uses native HLS or ordinary video; the media-level measurements still work.
53
+
54
+ ## Monitor options
55
+
56
+ The second argument to `monitor()` has the same options as `monitorHlsJs()`, `monitorVideoJs()`, and `monitorShaka()` below.
57
+
58
+ | Option | Required or default | Meaning |
59
+ | --- | --- | --- |
60
+ | `videoId` | Required | Your video ID or Chunkify asset ID. Every event includes it as `video_id`. |
61
+ | `destinations` | Required, non-empty array | Adapters or objects with `send(event)`. Each included event goes to every destination. |
62
+ | `snapshotIntervalMs` | `30000` | Active playing time between cumulative snapshots, in milliseconds. Must be an integer from `1000` to `2147483647`. |
63
+ | `includeEvents` | All six event types | Exact list of event types sent to destinations: `view_started`, `view_snapshot`, `playback_error`, `view_ended`, `playback_state_changed`, and `rendition_changed`. |
64
+ | `environment` | `null` | Optional application environment string included in events. |
65
+ | `viewerId` | `null` | Optional pseudonymous viewer ID included in events. No viewer identity is generated by default. |
66
+
67
+ An empty `includeEvents` array sends nothing. Excluding `view_snapshot` stops its timer but still collects the final view metrics. State changes and rendition switches are sent by default, so a view with many pauses, stalls, seeks, or quality changes sends more records. To receive only the original four event types, list those four in `includeEvents`.
68
+
69
+ ## Monitor third-party players
70
+
71
+ Use the connector for the player's public media API. All connectors take the same analytics options and return a session with `destroy()`.
72
+
73
+ ### hls.js
74
+
75
+ For a separate player that gives you its `Hls` instance, use the optional hls.js adapter. This example assumes the page has a `<video controls>` element. The adapter follows that element once hls.js attaches it:
76
+
77
+ ```ts
78
+ import Hls from "hls.js";
79
+ import { monitorHlsJs } from "@chunkify/analytics";
80
+
81
+ const video = document.querySelector("video")!;
82
+ const hls = new Hls();
83
+ const session = monitorHlsJs(hls, {
84
+ videoId: "video_123",
85
+ destinations: [{ send: (event) => console.log(event) }],
86
+ });
87
+ hls.attachMedia(video);
88
+ hls.loadSource("https://example.com/video.m3u8");
89
+
90
+ // Call session.sourceChanged() before replacing the source.
91
+ // When finished, call session.destroy() and hls.destroy().
92
+ ```
93
+
94
+ ### Video.js 10
95
+
96
+ For Video.js 10 HLS media (`<hls-video>`, `<hlsjs-video>`, or `<native-hls-video>`), pass the media element to `monitorVideoJs()`, not the outer `<video-player>`. The collector reads standard playback events from the element. With `<hlsjs-video>` using MSE, it also reads rendition and request metrics from the element's public hls.js `engine`. Native HLS and the lightweight `<hls-video>` expose fewer engine metrics, so those values remain null. For a plain `<video>` inside Video.js 10, use `monitor(video, options)`:
97
+
98
+ ```html
99
+ <video-player>
100
+ <video-skin style="display: block; width: 100%; aspect-ratio: 16 / 9">
101
+ <hlsjs-video src="/video.m3u8"></hlsjs-video>
102
+ </video-skin>
103
+ </video-player>
104
+ ```
105
+
106
+ ```ts
107
+ import "@videojs/html/video/player";
108
+ import "@videojs/html/video/skin";
109
+ import "@videojs/html/media/hlsjs-video";
110
+ import { monitorVideoJs } from "@chunkify/analytics";
111
+
112
+ const media = document.querySelector("hlsjs-video")!;
113
+ const session = monitorVideoJs(media, {
114
+ videoId: "video_123",
115
+ destinations: [{ send: (event) => console.log(event) }],
116
+ });
117
+ // Call session.destroy() when the media element is removed.
118
+ ```
119
+
120
+ This integration targets the [Video.js 10 HTML API](https://videojs.org/docs/framework/html/guides/architecture), which is currently a release candidate. It does not target Video.js 8. Video.js also defines an `<hls-video>` tag; that is different from the `hls-video-element` used by Chunkify Player. Use `monitor()` for `<chunkify-player>`.
121
+
122
+ ### Shaka Player
123
+
124
+ For Shaka Player, use a page with `<video controls>`, attach Shaka to it, then call `monitorShaka()` before `load()`. The connector starts each view on Shaka's `loading` event and enables Shaka's quality observer so it can measure playing time by video rendition at the playhead. It also counts completed network downloads, response bytes, download time, and critical error codes. It discards request URLs and response bodies. Cached responses are excluded from the delivery totals:
125
+
126
+ ```ts
127
+ import shaka from "shaka-player";
128
+ import { monitorShaka } from "@chunkify/analytics";
129
+
130
+ shaka.polyfill.installAll();
131
+ const video = document.querySelector("video")!;
132
+ const player = new shaka.Player();
133
+ await player.attach(video);
134
+ const session = monitorShaka(player, {
135
+ videoId: "video_123",
136
+ destinations: [{ send: (event) => console.log(event) }],
137
+ });
138
+ await player.load("https://example.com/video.m3u8");
139
+ // The viewer can now use the video's controls.
140
+
141
+ // Call session.destroy() before player.destroy().
142
+ ```
143
+
144
+ Shaka's `unloading` event ends the current view on a source change; its next `loading` event starts a fresh view. Call `session.sourceChanged()` yourself if you replace the source outside Shaka. The quality observer must be enabled before Shaka loads content; creating the session after `load()` may leave rendition playing time unavailable until the next load. `session.destroy()` restores the prior quality-observer setting if monitoring enabled it.
145
+ Shaka may reject `player.load()` without dispatching an `error` event. Handle that promise rejection in your application; only Shaka's emitted critical errors become `playback_error` records.
146
+ See [Shaka's setup guide](https://github.com/shaka-project/shaka-player/blob/main/docs/tutorials/basic-usage.md) for browser support, polyfills, and player setup.
147
+
148
+ ## Events and metrics
149
+
150
+ The six event names are `chunkify.video.view_started`, `chunkify.video.view_snapshot`, `chunkify.video.playback_error`, `chunkify.video.view_ended`, `chunkify.video.playback_state_changed`, and `chunkify.video.rendition_changed`. All records share identifiers, a timestamp, and a sequence number within the `view_id`. The first four contain cumulative view measurements. Snapshots normally arrive after each 30 seconds of active playing time. State and rendition records have smaller, event-specific payloads. A value is `null` when it is unavailable or does not apply; the collector does not guess it.
151
+
152
+ ### Complete Chunkify Player event example
153
+
154
+ This complete `view_ended` event is based on `<chunkify-player>` playing a local HLS stream, with timing values rounded for readability. The component used hls.js for this stream, so `connector_name` and `player_name` report `hls.js`. It is the **raw Chunkify event** passed to `send(event)`; an adapter or mapper can reshape it for a destination.
155
+
156
+ ```json
157
+ {
158
+ "schema_version": 1,
159
+ "event_name": "chunkify.video.view_ended",
160
+ "event_id": "06581e06-96d7-421c-b77b-22a6e615c97e",
161
+ "sequence": 4,
162
+ "occurred_at": "2026-09-28T07:35:10.879Z",
163
+ "view_id": "8267fd81-ecf6-4fb1-83d9-4d1fba46b1b6",
164
+ "video_id": "video_123",
165
+ "sdk_name": "@chunkify/analytics",
166
+ "sdk_version": "0.1.0",
167
+ "connector_name": "hls.js",
168
+ "connector_version": "1.7.3",
169
+ "player_name": "hls.js",
170
+ "player_version": "1.7.3",
171
+ "delivery_hostname": null,
172
+ "environment": null,
173
+ "viewer_id": null,
174
+ "reason": "ended",
175
+ "startup_outcome": "started",
176
+ "error_category": null,
177
+ "error_code": null,
178
+ "error_fatal": null,
179
+ "error_playhead_seconds": null,
180
+ "media_duration_seconds": 6.021333,
181
+ "startup_time_ms": 2,
182
+ "first_video_frame_time_ms": 114,
183
+ "playing_time_ms": 6086,
184
+ "rebuffer_count": 0,
185
+ "rebuffer_duration_ms": 0,
186
+ "rebuffer_ratio": 0,
187
+ "seek_count": 0,
188
+ "seek_duration_ms": 0,
189
+ "maximum_playhead_seconds": 6.021333,
190
+ "furthest_progress_ratio": 1,
191
+ "video_intrinsic_width_css_px": 256,
192
+ "video_intrinsic_height_css_px": 144,
193
+ "dropped_video_frames": 0,
194
+ "total_video_frames": 68,
195
+ "selected_rendition_width": 256,
196
+ "selected_rendition_height": 144,
197
+ "selected_rendition_codec": null,
198
+ "selected_rendition_bitrate_bps": 250000,
199
+ "rendition_switch_count": 0,
200
+ "delivery_request_count": 2,
201
+ "delivery_transferred_bytes": 96329,
202
+ "delivery_throughput_bps": 175143637,
203
+ "rendition_playing_time": [
204
+ {
205
+ "width": 256,
206
+ "height": 144,
207
+ "codec": null,
208
+ "bitrate_bps": 250000,
209
+ "frame_rate": null,
210
+ "playing_time_ms": 6085
211
+ }
212
+ ]
213
+ }
214
+ ```
215
+
216
+ ### Playback timeline events
217
+
218
+ `playback_state_changed` reports the state entered and the wall-clock time spent in the previous state. Possible states are `startup` (the first attempt before playback), `resuming` (waiting after a pause or seek), `playing`, `paused`, `buffering`, `seeking`, `error`, `ended`, and `stopped`. The final transition to `ended` or `stopped` closes the last state before `view_ended`. A fatal error enters `error`; `view_ended` still reports why the view ended. For example, pausing after 20 seconds of playback produces these event-specific fields:
219
+
220
+ ```json
221
+ {
222
+ "event_name": "chunkify.video.playback_state_changed",
223
+ "from_state": "playing",
224
+ "to_state": "paused",
225
+ "previous_state_duration_ms": 20000,
226
+ "playhead_seconds": 42
227
+ }
228
+ ```
229
+
230
+ `rendition_changed` reports a change between two known video renditions. It does not fire for the initial selection, repeated reports of the same rendition, or engines that do not expose rendition changes. The event has `previous_rendition_` and `selected_rendition_` fields for width, height, codec, bitrate in bits per second, and frame rate. Unknown properties are `null`:
231
+
232
+ ```json
233
+ {
234
+ "event_name": "chunkify.video.rendition_changed",
235
+ "playhead_seconds": 42,
236
+ "previous_rendition_width": 1280,
237
+ "previous_rendition_height": 720,
238
+ "selected_rendition_width": 854,
239
+ "selected_rendition_height": 480
240
+ }
241
+ ```
242
+
243
+ These are excerpts; actual timeline records also have the shared identifiers and timestamp. `playhead_seconds` is the media element's `currentTime` at the transition, or `null` if unavailable. It is a media position, not elapsed viewing time or a universal timestamp for live video. `occurred_at` and `sequence` place the transition in the viewing timeline. Cumulative playing time by rendition remains in snapshots and `view_ended`.
244
+
245
+ ### Field guide
246
+
247
+ | Fields | Meaning |
248
+ | --- | --- |
249
+ | `schema_version`, `event_name`, `event_id`, `sequence`, `occurred_at` | Contract version, event type, unique event ID, sequence within this view, and UTC event time. |
250
+ | `view_id`, `video_id` | Ephemeral playback-attempt ID and the `videoId` supplied by your application. A new attempt gets a new `view_id`. |
251
+ | `sdk_name`, `sdk_version`, `connector_name`, `connector_version`, `player_name`, `player_version` | The collector, connector, and player that produced the record. Player fields can be `null`. |
252
+ | `delivery_hostname`, `environment`, `viewer_id` | Delivery host when available, optional application environment, and optional pseudonymous `viewerId` supplied by the application. No complete URL or viewer identity is generated. |
253
+ | `reason`, `startup_outcome` | End reason and startup result on `view_ended`; `null` on other events. Ending before startup does not establish why the viewer left. |
254
+ | `error_category`, `error_code`, `error_fatal`, `error_playhead_seconds` | Normalized error details on `playback_error`; otherwise `null`. Raw error messages and URLs are not included. |
255
+ | `media_duration_seconds`, `startup_time_ms`, `first_video_frame_time_ms`, `playing_time_ms` | Media duration; time from attempt to first `playing`; time to the first frame callback; cumulative actual playing time. The two startup measurements can differ. |
256
+ | `rebuffer_count`, `rebuffer_duration_ms`, `rebuffer_ratio`, `seek_count`, `seek_duration_ms` | Counts, cumulative durations, and rebuffer time divided by playing plus rebuffer time. The ratio is `null` until its denominator is positive. |
257
+ | `maximum_playhead_seconds`, `furthest_progress_ratio` | Furthest playhead reached and that position divided by duration, capped at 1. Seeking near the end raises the ratio without watching the intervening video. |
258
+ | `video_intrinsic_width_css_px`, `video_intrinsic_height_css_px`, `dropped_video_frames`, `total_video_frames` | Intrinsic display dimensions and browser frame counters when exposed. The dimensions are not necessarily encoded frame dimensions. |
259
+ | `selected_rendition_width`, `selected_rendition_height`, `selected_rendition_codec`, `selected_rendition_bitrate_bps`, `rendition_switch_count` | Current video rendition and switches when an engine connector exposes them. Width and height are in pixels; bitrate is in bits per second. |
260
+ | `rendition_playing_time` | Cumulative wall-clock milliseconds by rendition. Each entry has `width`, `height`, `codec`, `bitrate_bps`, `frame_rate`, and `playing_time_ms`. It is `null` without a rendition-aware connector and `[]` before one identifies a rendition. |
261
+ | `delivery_request_count`, `delivery_transferred_bytes`, `delivery_throughput_bps` | Aggregate requests, response bytes, and throughput where the engine exposes them. Throughput is in bits per second. |
262
+
263
+ `reason` can be `ended`, `destroyed`, `source_changed`, or `page_exit`. `startup_outcome` can be `started`, `failed_before_start`, or `ended_before_start`. Error categories are `aborted`, `network`, `decode`, `unsupported`, or `unknown`.
264
+
265
+ `first_video_frame_time_ms` uses `requestVideoFrameCallback()` and may remain `null` or precede `startup_time_ms` for a preloaded frame. It is not proof that the viewer saw the frame. `rendition_playing_time` excludes pauses, buffering, seeks, and playback before a rendition is known, so its sum can be lower than `playing_time_ms`. Snapshots repeat cumulative totals: use the final record or differences between snapshots, not the sum of whole snapshots. This package does not aggregate across viewers.
266
+
267
+ ## Map event fields
268
+
269
+ Use `createEventMapper()` when a destination needs different field names or values. Each output field names a source event field or calculates a value from the event. The mapper returns a new object and leaves the original event unchanged:
270
+
271
+ ```ts
272
+ import { createEventMapper, monitor } from "@chunkify/analytics";
273
+ import { createHttpAdapter } from "@chunkify/analytics/http";
274
+
275
+ const mapForEndpoint = createEventMapper({
276
+ videoId: "video_id",
277
+ viewId: "view_id",
278
+ eventName: "event_name",
279
+ playtimeSeconds: (event) => event.playing_time_ms === undefined ? undefined : event.playing_time_ms / 1000,
280
+ });
281
+
282
+ monitor(video, {
283
+ videoId: "video_123",
284
+ destinations: [createHttpAdapter("/api/video-analytics", mapForEndpoint)],
285
+ });
286
+ ```
287
+
288
+ The mapping runs for each event passed to the destination. A field absent from an event-specific payload is omitted from the mapped result, including when a selector returns `undefined`. For example, `playtimeSeconds` above is omitted from a rendition-change event. For destinations that accept the Chunkify event directly, omit the mapper. Mapping does not alter collection, event selection, or snapshot timing. See [Monitor options](#monitor-options) for `includeEvents` and `snapshotIntervalMs`.
289
+
290
+ To map event types differently, select a mapper in the adapter. This example assumes the Datadog RUM client has already been initialized. Other events keep their original fields:
291
+
292
+ ```js
293
+ import { datadogRum } from "@datadog/browser-rum";
294
+ import { createEventMapper } from "@chunkify/analytics";
295
+ import { createDatadogAdapter } from "@chunkify/analytics/datadog";
296
+
297
+ const mapStateChange = createEventMapper({
298
+ videoId: "video_id",
299
+ from: "from_state",
300
+ to: "to_state",
301
+ durationMs: "previous_state_duration_ms",
302
+ });
303
+ const mapRenditionChange = createEventMapper({
304
+ videoId: "video_id",
305
+ width: "selected_rendition_width",
306
+ height: "selected_rendition_height",
307
+ });
308
+
309
+ const destination = createDatadogAdapter(datadogRum, (event) => {
310
+ if (event.event_name === "chunkify.video.playback_state_changed") return mapStateChange(event);
311
+ if (event.event_name === "chunkify.video.rendition_changed") return mapRenditionChange(event);
312
+ return { ...event };
313
+ });
314
+ ```
315
+
316
+ Pass `destination` in `monitor()`'s `destinations` array. The same pattern works with the PostHog and HTTP adapters.
317
+
318
+ ## Send events to a destination
319
+
320
+ Destinations keep collection separate from delivery. Provide at least one adapter or object with `send(event)`. Every included event goes to every destination. The package does not retry, queue, or guarantee delivery on page exit. Do not put personal information, tokens, or URLs in supplied metadata. The event payload omits raw URLs and error messages. The Datadog and PostHog examples use [the player markup](#monitor-chunkify-player-or-a-video-element) shown above.
321
+
322
+ ### Datadog RUM
323
+
324
+ Install `@datadog/browser-rum` in your application. Initialize it with your RUM application ID and **client token** (not a Datadog API key), then pass the client to the adapter. The adapter adds one custom action per analytics event, with the event fields directly in the action context by default:
325
+
326
+ ```js
327
+ import "@chunkify/player";
328
+ import { datadogRum } from "@datadog/browser-rum";
329
+ import { monitor } from "@chunkify/analytics";
330
+ import { createDatadogAdapter } from "@chunkify/analytics/datadog";
331
+
332
+ datadogRum.init({
333
+ applicationId: "YOUR_DATADOG_APPLICATION_ID",
334
+ clientToken: "YOUR_DATADOG_RUM_CLIENT_TOKEN",
335
+ site: "datadoghq.com", // Use your organization's Datadog site.
336
+ });
337
+
338
+ monitor(document.querySelector("#player"), {
339
+ videoId: "video_123",
340
+ destinations: [createDatadogAdapter(datadogRum)],
341
+ });
342
+ ```
343
+
344
+ Pass a mapper as the second argument to choose or rename context fields; it runs for each event. If a dashboard needs derived actions, such as one action per rendition, add a separate custom destination to create them from `view_ended`.
345
+
346
+ ### PostHog
347
+
348
+ Install `posthog-js` in your application. Initialize it with your project token and ingestion host, then pass it to the adapter. The adapter captures one event per analytics record, with the event's fields as flat PostHog properties. Its optional second argument maps the properties for each event:
349
+
350
+ ```js
351
+ import "@chunkify/player";
352
+ import posthog from "posthog-js";
353
+ import { monitor } from "@chunkify/analytics";
354
+ import { createPostHogAdapter } from "@chunkify/analytics/posthog";
355
+
356
+ posthog.init("YOUR_POSTHOG_PROJECT_TOKEN", {
357
+ api_host: "https://us.i.posthog.com", // Use your project's ingestion host.
358
+ });
359
+
360
+ monitor(document.querySelector("#player"), {
361
+ videoId: "video_123",
362
+ destinations: [createPostHogAdapter(posthog)],
363
+ });
364
+ ```
365
+
366
+ The application controls PostHog's automatic collection and persistence settings. PostHog may add its own event properties, including page context, beyond the fields sent by this adapter; configure the PostHog SDK separately to meet your privacy requirements.
367
+
368
+ ### OTLP/HTTP logs
369
+
370
+ The OTLP adapter sends each event as an OTLP/HTTP JSON log record. Pass the full URL of a customer-owned `/v1/logs` gateway, or a root-relative path to one. The gateway must allow browser CORS when cross-origin and handle any private provider credentials. The adapter sends no cookies or authorization headers. By default, all event fields become typed log attributes; unavailable values are omitted, and `rendition_playing_time` remains a structured array. Pass a mapper as the second argument to choose or rename log attributes. The OTLP event name, timestamp, and scope still come from the original event. Snapshots contain cumulative values, so aggregate the final record or differences between snapshots rather than summing all records:
371
+
372
+ ```ts
373
+ import { createEventMapper } from "@chunkify/analytics";
374
+ import { createOtlpAdapter } from "@chunkify/analytics/otlp";
375
+
376
+ const attributes = createEventMapper({
377
+ video_id: "video_id",
378
+ view_id: "view_id",
379
+ event_id: "event_id",
380
+ playing_time_ms: "playing_time_ms",
381
+ });
382
+
383
+ monitor(video, {
384
+ videoId: "video_123",
385
+ destinations: [createOtlpAdapter("https://analytics.example.com/v1/logs", attributes)],
386
+ });
387
+ ```
388
+
389
+ This adapter emits OTLP **logs**, not OTLP metrics. Your gateway or provider handles storage and aggregation.
390
+
391
+ ### HTTP endpoint
392
+
393
+ The HTTP adapter posts each event as JSON to a customer-owned endpoint using `fetch`. Pass a mapper as its second argument when the endpoint needs a different payload; otherwise it sends the complete Chunkify event. It accepts an HTTPS URL or root-relative path; loopback HTTP is allowed for local tests. It rejects credentials and query parameters in the endpoint, sends no cookies, and sends no page referrer. The endpoint must support browser CORS when cross-origin and perform its own aggregation. Non-2xx responses reject the adapter's `send()` promise; the collector isolates delivery failures and does not retry them. The final event uses `fetch` keepalive as a best-effort page-exit delivery, subject to browser limits:
394
+
395
+ ```ts
396
+ import { createHttpAdapter } from "@chunkify/analytics/http";
397
+
398
+ monitor(video, {
399
+ videoId: "video_123",
400
+ destinations: [createHttpAdapter("/api/video-analytics")],
401
+ });
402
+ ```
403
+
404
+ ### Custom destination
405
+
406
+ No adapter is needed when you want the normalized event as-is. A destination can inspect it, map it, or pass it to your own client:
407
+
408
+ ```ts
409
+ monitor(video, {
410
+ videoId: "video_123",
411
+ destinations: [{ send(event) { console.log(event); } }],
412
+ });
413
+ ```
package/dist/core.d.ts ADDED
@@ -0,0 +1,144 @@
1
+ import { type AnalyticsEventType } from "./event-types.js";
2
+ export type { AnalyticsEventType } from "./event-types.js";
3
+ export type AnalyticsEndReason = "ended" | "destroyed" | "source_changed" | "page_exit";
4
+ export type AnalyticsErrorCategory = "aborted" | "network" | "decode" | "unsupported" | "unknown";
5
+ export type AnalyticsStartupOutcome = "started" | "failed_before_start" | "ended_before_start";
6
+ export type AnalyticsPlaybackState = "startup" | "resuming" | "playing" | "paused" | "buffering" | "seeking" | "error" | "ended" | "stopped";
7
+ export interface RenditionPlayingTime {
8
+ width: number | null;
9
+ height: number | null;
10
+ codec: string | null;
11
+ bitrate_bps: number | null;
12
+ frame_rate: number | null;
13
+ playing_time_ms: number;
14
+ }
15
+ export type AnalyticsRendition = Omit<RenditionPlayingTime, "playing_time_ms">;
16
+ export interface ViewMetrics {
17
+ media_duration_seconds: number | null;
18
+ startup_time_ms: number | null;
19
+ first_video_frame_time_ms: number | null;
20
+ playing_time_ms: number;
21
+ rebuffer_count: number;
22
+ rebuffer_duration_ms: number;
23
+ rebuffer_ratio: number | null;
24
+ seek_count: number;
25
+ seek_duration_ms: number;
26
+ maximum_playhead_seconds: number;
27
+ furthest_progress_ratio: number | null;
28
+ video_intrinsic_width_css_px: number | null;
29
+ video_intrinsic_height_css_px: number | null;
30
+ dropped_video_frames: number | null;
31
+ total_video_frames: number | null;
32
+ selected_rendition_width: number | null;
33
+ selected_rendition_height: number | null;
34
+ selected_rendition_codec: string | null;
35
+ selected_rendition_bitrate_bps: number | null;
36
+ rendition_switch_count: number | null;
37
+ rendition_playing_time: RenditionPlayingTime[] | null;
38
+ delivery_request_count: number | null;
39
+ delivery_transferred_bytes: number | null;
40
+ delivery_throughput_bps: number | null;
41
+ }
42
+ export interface AnalyticsEventEnvelope {
43
+ schema_version: 1;
44
+ event_name: `chunkify.video.${AnalyticsEventType}`;
45
+ event_id: string;
46
+ sequence: number;
47
+ occurred_at: string;
48
+ view_id: string;
49
+ video_id: string;
50
+ sdk_name: "@chunkify/analytics";
51
+ sdk_version: string;
52
+ connector_name: string;
53
+ connector_version: string | null;
54
+ player_name: string | null;
55
+ player_version: string | null;
56
+ delivery_hostname: string | null;
57
+ environment: string | null;
58
+ viewer_id: string | null;
59
+ reason: AnalyticsEndReason | null;
60
+ startup_outcome: AnalyticsStartupOutcome | null;
61
+ error_category: AnalyticsErrorCategory | null;
62
+ error_code: string | number | null;
63
+ error_fatal: boolean | null;
64
+ error_playhead_seconds: number | null;
65
+ }
66
+ export interface AnalyticsViewEvent extends AnalyticsEventEnvelope, ViewMetrics {
67
+ event_name: "chunkify.video.view_started" | "chunkify.video.view_snapshot" | "chunkify.video.playback_error" | "chunkify.video.view_ended";
68
+ }
69
+ export interface AnalyticsPlaybackStateEvent extends AnalyticsEventEnvelope, Partial<ViewMetrics> {
70
+ event_name: "chunkify.video.playback_state_changed";
71
+ playhead_seconds: number | null;
72
+ from_state: AnalyticsPlaybackState;
73
+ to_state: AnalyticsPlaybackState;
74
+ previous_state_duration_ms: number;
75
+ }
76
+ export interface AnalyticsRenditionEvent extends AnalyticsEventEnvelope, Partial<ViewMetrics> {
77
+ event_name: "chunkify.video.rendition_changed";
78
+ playhead_seconds: number | null;
79
+ previous_rendition_width: number | null;
80
+ previous_rendition_height: number | null;
81
+ previous_rendition_codec: string | null;
82
+ previous_rendition_bitrate_bps: number | null;
83
+ previous_rendition_frame_rate: number | null;
84
+ selected_rendition_width: number | null;
85
+ selected_rendition_height: number | null;
86
+ selected_rendition_codec: string | null;
87
+ selected_rendition_bitrate_bps: number | null;
88
+ selected_rendition_frame_rate: number | null;
89
+ }
90
+ export type ChunkifyPlayerAnalyticsEvent = AnalyticsViewEvent | AnalyticsPlaybackStateEvent | AnalyticsRenditionEvent;
91
+ export interface AnalyticsDestination {
92
+ send(event: ChunkifyPlayerAnalyticsEvent): void | Promise<void>;
93
+ }
94
+ export interface AnalyticsConfig {
95
+ destinations: readonly AnalyticsDestination[];
96
+ environment?: string | undefined;
97
+ viewerId?: string | undefined;
98
+ /** Active playing time between cumulative snapshots. Defaults to 30,000 ms. */
99
+ snapshotIntervalMs?: number | undefined;
100
+ /** Event types passed to destinations. Defaults to all six types. */
101
+ includeEvents?: readonly AnalyticsEventType[] | undefined;
102
+ }
103
+ export interface AnalyticsOptions extends AnalyticsConfig {
104
+ videoId: string;
105
+ }
106
+ export interface AnalyticsConnectorSnapshot {
107
+ renditionWidth: number | null;
108
+ renditionHeight: number | null;
109
+ renditionCodec: string | null;
110
+ renditionBitrate: number | null;
111
+ renditionSwitchCount: number | null;
112
+ deliveryRequestCount: number | null;
113
+ deliveryTransferredBytes: number | null;
114
+ deliveryDurationMs: number | null;
115
+ }
116
+ export interface AnalyticsConnectorError {
117
+ category: AnalyticsErrorCategory;
118
+ code: string | number | null;
119
+ fatal: boolean;
120
+ }
121
+ export interface AnalyticsConnector {
122
+ name: string;
123
+ version: string | null;
124
+ playerName: string | null;
125
+ playerVersion: string | null;
126
+ snapshot(): AnalyticsConnectorSnapshot;
127
+ subscribeToErrors(listener: (error: AnalyticsConnectorError) => void): () => void;
128
+ /** Fires with the rendition currently reaching playback, including the initial selection. */
129
+ subscribeToRenditions?(listener: (rendition: AnalyticsRendition | null) => void): () => void;
130
+ }
131
+ export interface AnalyticsSession {
132
+ /** Call immediately before play() to include attempts that reject before a play event. */
133
+ startAttempt(): void;
134
+ /** Finish the current view before its media source or playback engine is replaced. */
135
+ sourceChanged(): void;
136
+ /** Add or replace optional engine-specific measurements for subsequent records. */
137
+ setConnector(connector: AnalyticsConnector | null): void;
138
+ /** End the current view, if any, and remove all analytics listeners. Idempotent. */
139
+ destroy(): void;
140
+ }
141
+ export declare function attachAnalytics(media: HTMLMediaElement, options: AnalyticsOptions): AnalyticsSession;
142
+ /** Engine adapters call this when they own source-change events. */
143
+ export declare function attachAnalyticsWithManagedSource(media: HTMLMediaElement, options: AnalyticsOptions): AnalyticsSession;
144
+ //# sourceMappingURL=core.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"core.d.ts","sourceRoot":"","sources":["../src/core.ts"],"names":[],"mappings":"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"}