@eyevinn/player-analytics-client-sdk-web 0.3.0 → 0.4.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.
@@ -0,0 +1,338 @@
1
+ import {
2
+ PlayerAnalytics,
3
+ IPlayerAnalyticsInitOptions,
4
+ } from "../src/PlayerAnalytics";
5
+ import {
6
+ TPlayingEvent,
7
+ TPausedEvent,
8
+ TBufferingEvent,
9
+ TErrorEvent,
10
+ TMetadataEvent,
11
+ } from "@eyevinn/player-analytics-specification";
12
+
13
+ describe("PlayerAnalytics", () => {
14
+ let mockFetch: jasmine.Spy;
15
+ let originalFetch: any;
16
+
17
+ beforeEach(() => {
18
+ originalFetch = globalThis.fetch;
19
+ mockFetch = jasmine.createSpy("fetch").and.returnValue(
20
+ Promise.resolve({
21
+ ok: true,
22
+ json: () => Promise.resolve({ sessionId: "test-session-id" }),
23
+ statusText: "OK",
24
+ } as Response)
25
+ );
26
+ globalThis.fetch = mockFetch;
27
+ });
28
+
29
+ afterEach(() => {
30
+ globalThis.fetch = originalFetch;
31
+ });
32
+
33
+ describe("initiateAnalyticsReporter()", () => {
34
+ it("should create Reporter and call init", async () => {
35
+ const analytics = new PlayerAnalytics("https://example.com/analytics");
36
+
37
+ // Mock returns empty response, so provided sessionId is kept
38
+ mockFetch.and.returnValue(
39
+ Promise.resolve({
40
+ ok: true,
41
+ json: () => Promise.resolve({}),
42
+ statusText: "OK",
43
+ } as Response)
44
+ );
45
+
46
+ const options: IPlayerAnalyticsInitOptions = {
47
+ sessionId: "my-session-id",
48
+ heartbeatInterval: 30000,
49
+ };
50
+
51
+ const result = await analytics.initiateAnalyticsReporter(options);
52
+
53
+ expect(result.sessionId).toBe("my-session-id");
54
+ expect(result.heartbeatInterval).toBe(30000);
55
+ expect(result.isInitiated).toBe(true);
56
+ expect(mockFetch).toHaveBeenCalled();
57
+ });
58
+
59
+ it("should use default heartbeatInterval when not provided", async () => {
60
+ const analytics = new PlayerAnalytics("https://example.com/analytics");
61
+
62
+ const result = await analytics.initiateAnalyticsReporter({});
63
+
64
+ expect(result.heartbeatInterval).toBe(30000); // HEARTBEAT_INTERVAL constant
65
+ });
66
+
67
+ it("should pass shardId to Reporter", async () => {
68
+ const analytics = new PlayerAnalytics("https://example.com/analytics");
69
+
70
+ const options: IPlayerAnalyticsInitOptions = {
71
+ sessionId: "session-123",
72
+ shardId: "shard-456",
73
+ };
74
+
75
+ await analytics.initiateAnalyticsReporter(options);
76
+
77
+ // Verify shardId is included in init request
78
+ const bodyString = mockFetch.calls.argsFor(0)[1].body;
79
+ const body = JSON.parse(bodyString);
80
+ expect(body.shardId).toBe("shard-456");
81
+ });
82
+ });
83
+
84
+ describe("event methods before init", () => {
85
+ it("should warn and return when calling playing() before init", () => {
86
+ const analytics = new PlayerAnalytics("https://example.com/analytics");
87
+ spyOn(console, "warn");
88
+
89
+ const eventData: TPlayingEvent = {
90
+ event: "playing",
91
+ sessionId: "test",
92
+ timestamp: Date.now(),
93
+ playhead: 0,
94
+ duration: 100,
95
+ };
96
+
97
+ analytics.playing(eventData);
98
+
99
+ expect(console.warn).toHaveBeenCalledWith(
100
+ "[PlayerAnalytics] Not initialized. Call initiateAnalyticsReporter() first."
101
+ );
102
+ });
103
+
104
+ it("should warn and return when calling pause() before init", () => {
105
+ const analytics = new PlayerAnalytics("https://example.com/analytics");
106
+ spyOn(console, "warn");
107
+
108
+ const eventData: TPausedEvent = {
109
+ event: "paused",
110
+ sessionId: "test",
111
+ timestamp: Date.now(),
112
+ playhead: 5,
113
+ duration: 100,
114
+ };
115
+
116
+ analytics.pause(eventData);
117
+
118
+ expect(console.warn).toHaveBeenCalled();
119
+ });
120
+
121
+ it("should warn and return when calling buffering() before init", () => {
122
+ const analytics = new PlayerAnalytics("https://example.com/analytics");
123
+ spyOn(console, "warn");
124
+
125
+ const eventData: TBufferingEvent = {
126
+ event: "buffering",
127
+ sessionId: "test",
128
+ timestamp: Date.now(),
129
+ playhead: 10,
130
+ duration: 100,
131
+ };
132
+
133
+ analytics.buffering(eventData);
134
+
135
+ expect(console.warn).toHaveBeenCalled();
136
+ });
137
+ });
138
+
139
+ describe("event methods after init", () => {
140
+ let analytics: PlayerAnalytics;
141
+
142
+ beforeEach(async () => {
143
+ analytics = new PlayerAnalytics("https://example.com/analytics");
144
+ await analytics.initiateAnalyticsReporter({ sessionId: "test-session" });
145
+ mockFetch.calls.reset();
146
+ });
147
+
148
+ it("should call reporter.send with correct data for playing()", () => {
149
+ const eventData: TPlayingEvent = {
150
+ event: "playing",
151
+ sessionId: "test-session",
152
+ timestamp: 1234567890,
153
+ playhead: 0,
154
+ duration: 100,
155
+ };
156
+
157
+ analytics.playing(eventData);
158
+
159
+ expect(mockFetch).toHaveBeenCalled();
160
+ const body = JSON.parse(mockFetch.calls.argsFor(0)[1].body);
161
+ expect(body.event).toBe("playing");
162
+ // Reporter uses its authoritative sessionId (from server init response)
163
+ expect(body.sessionId).toBe("test-session-id");
164
+ });
165
+
166
+ it("should call reporter.send with correct data for pause()", () => {
167
+ const eventData: TPausedEvent = {
168
+ event: "paused",
169
+ sessionId: "test-session",
170
+ timestamp: 1234567890,
171
+ playhead: 15,
172
+ duration: 100,
173
+ };
174
+
175
+ analytics.pause(eventData);
176
+
177
+ expect(mockFetch).toHaveBeenCalled();
178
+ const body = JSON.parse(mockFetch.calls.argsFor(0)[1].body);
179
+ expect(body.event).toBe("paused");
180
+ expect(body.playhead).toBe(15);
181
+ });
182
+
183
+ it("should call reporter.send with correct data for buffering()", () => {
184
+ const eventData: TBufferingEvent = {
185
+ event: "buffering",
186
+ sessionId: "test-session",
187
+ timestamp: 1234567890,
188
+ playhead: 20,
189
+ duration: 100,
190
+ };
191
+
192
+ analytics.buffering(eventData);
193
+
194
+ expect(mockFetch).toHaveBeenCalled();
195
+ const body = JSON.parse(mockFetch.calls.argsFor(0)[1].body);
196
+ expect(body.event).toBe("buffering");
197
+ });
198
+
199
+ it("should call reporter.send with correct data for error()", () => {
200
+ const eventData: TErrorEvent = {
201
+ event: "error",
202
+ sessionId: "test-session",
203
+ timestamp: 1234567890,
204
+ playhead: 25,
205
+ duration: 100,
206
+ payload: {
207
+ category: "network",
208
+ code: "MEDIA_ERR_NETWORK",
209
+ message: "Failed to load media",
210
+ },
211
+ };
212
+
213
+ analytics.error(eventData);
214
+
215
+ expect(mockFetch).toHaveBeenCalled();
216
+ const body = JSON.parse(mockFetch.calls.argsFor(0)[1].body);
217
+ expect(body.event).toBe("error");
218
+ expect(body.payload.category).toBe("network");
219
+ });
220
+
221
+ it("should call reporter.send with correct data for metadata()", () => {
222
+ const eventData: TMetadataEvent = {
223
+ event: "metadata",
224
+ sessionId: "test-session",
225
+ timestamp: 1234567890,
226
+ playhead: 0,
227
+ duration: 100,
228
+ payload: {
229
+ contentId: "content-123",
230
+ contentUrl: "https://example.com/video.m3u8",
231
+ live: false,
232
+ },
233
+ };
234
+
235
+ analytics.metadata(eventData);
236
+
237
+ expect(mockFetch).toHaveBeenCalled();
238
+ const body = JSON.parse(mockFetch.calls.argsFor(0)[1].body);
239
+ expect(body.event).toBe("metadata");
240
+ expect(body.payload.contentId).toBe("content-123");
241
+ });
242
+
243
+ it("should call reporter.send for all event types", () => {
244
+ const baseEvent = {
245
+ sessionId: "test-session",
246
+ timestamp: Date.now(),
247
+ playhead: 0,
248
+ duration: 100,
249
+ };
250
+
251
+ analytics.loading({ event: "loading", ...baseEvent });
252
+ analytics.loaded({ event: "loaded", ...baseEvent });
253
+ analytics.seeking({ event: "seeking", ...baseEvent });
254
+ analytics.seeked({ event: "seeked", ...baseEvent });
255
+ analytics.buffered({ event: "buffered", ...baseEvent });
256
+ analytics.bitrateChanged({
257
+ event: "bitrate_changed",
258
+ ...baseEvent,
259
+ payload: { bitrate: 5000000 },
260
+ });
261
+ analytics.warning({
262
+ event: "warning",
263
+ ...baseEvent,
264
+ payload: { category: "player", code: "WARN_001", message: "Warning" },
265
+ });
266
+ analytics.stopped({
267
+ event: "stopped",
268
+ ...baseEvent,
269
+ payload: { reason: "ended" },
270
+ });
271
+ analytics.heartbeat({ event: "heartbeat", ...baseEvent });
272
+
273
+ expect(mockFetch).toHaveBeenCalledTimes(9);
274
+ });
275
+ });
276
+
277
+ describe("destroy()", () => {
278
+ it("should nullify reporter", async () => {
279
+ const analytics = new PlayerAnalytics("https://example.com/analytics");
280
+ await analytics.initiateAnalyticsReporter({ sessionId: "test-session" });
281
+
282
+ analytics.destroy();
283
+
284
+ spyOn(console, "warn");
285
+ analytics.playing({
286
+ event: "playing",
287
+ sessionId: "test",
288
+ timestamp: Date.now(),
289
+ playhead: 0,
290
+ duration: 100,
291
+ });
292
+
293
+ expect(console.warn).toHaveBeenCalledWith(
294
+ "[PlayerAnalytics] Not initialized. Call initiateAnalyticsReporter() first."
295
+ );
296
+ });
297
+ });
298
+
299
+ describe("debug mode", () => {
300
+ it("should not call fetch when debug mode is enabled", async () => {
301
+ const analytics = new PlayerAnalytics(
302
+ "https://example.com/analytics",
303
+ true
304
+ );
305
+
306
+ spyOn(console, "log");
307
+
308
+ await analytics.initiateAnalyticsReporter({ sessionId: "debug-session" });
309
+
310
+ expect(mockFetch).not.toHaveBeenCalled();
311
+ expect(console.log).toHaveBeenCalled();
312
+ });
313
+
314
+ it("should log events to console in debug mode", async () => {
315
+ const analytics = new PlayerAnalytics(
316
+ "https://example.com/analytics",
317
+ true
318
+ );
319
+
320
+ spyOn(console, "log");
321
+
322
+ await analytics.initiateAnalyticsReporter({ sessionId: "debug-session" });
323
+
324
+ const eventData: TPlayingEvent = {
325
+ event: "playing",
326
+ sessionId: "debug-session",
327
+ timestamp: 1234567890,
328
+ playhead: 0,
329
+ duration: 100,
330
+ };
331
+
332
+ analytics.playing(eventData);
333
+
334
+ expect(console.log).toHaveBeenCalled();
335
+ expect(mockFetch).not.toHaveBeenCalled();
336
+ });
337
+ });
338
+ });