Skip to content

Commit a304d17

Browse files
committed
Fix crashes due to large content time jumps in updateDurationStats in media tracking
Add a MAX_CONTENT_TIME_STEP (3600s) gap guard to the playedSeconds fill loop in updateDurationStats. When contentTime jumps beyond this threshold (e.g. live stream epoch offsets or large VOD seeks), the per-second loop is skipped and only the current second is recorded, preventing RangeError crashes from unbounded Set growth or billions of iterations.
1 parent 38c9ca0 commit a304d17

File tree

3 files changed

+62
-2
lines changed

3 files changed

+62
-2
lines changed
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
{
2+
"changes": [
3+
{
4+
"packageName": "@snowplow/browser-plugin-media",
5+
"comment": "Fix crashes due to large content time jumps in updateDurationStats in media tracking",
6+
"type": "none"
7+
}
8+
],
9+
"packageName": "@snowplow/browser-plugin-media"
10+
}

plugins/browser-plugin-media/src/sessionStats.ts

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,12 @@ type Log = {
1717
linearAd: boolean;
1818
};
1919

20+
/** Maximum content-time gap (seconds) for which intermediate seconds are filled into playedSeconds.
21+
* Beyond this threshold only the current second is recorded, preventing unbounded iteration
22+
* for live streams or large VOD seeks where currentTime can be a huge offset.
23+
*/
24+
const MAX_CONTENT_TIME_STEP = 3600;
25+
2026
const adStartTypes = [MediaEventType.AdStart, MediaEventType.AdResume];
2127
const adProgressTypes = [
2228
MediaEventType.AdClick,
@@ -134,9 +140,13 @@ export class MediaSessionTrackingStats {
134140
}
135141

136142
if (!log.paused) {
137-
for (let i = Math.floor(this.lastLog.contentTime); i < log.contentTime; i++) {
138-
this.playedSeconds.add(i);
143+
const gap = log.contentTime - this.lastLog.contentTime;
144+
if (gap <= MAX_CONTENT_TIME_STEP) {
145+
for (let i = Math.floor(this.lastLog.contentTime); i < log.contentTime; i++) {
146+
this.playedSeconds.add(i);
147+
}
139148
}
149+
// gap > MAX_CONTENT_TIME_STEP: skip loop, only current second is recorded below
140150
}
141151
}
142152
}

plugins/browser-plugin-media/test/sessionStats.test.ts

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -188,6 +188,46 @@ describe('MediaSessionTrackingStats', () => {
188188
expect(entity.timePlayedMuted).toBe(30);
189189
});
190190

191+
it('does not crash when content time jump exceeds MAX_CONTENT_TIME_STEP', () => {
192+
let session = new MediaSessionTrackingStats();
193+
194+
session.update(MediaEventType.Play, { ...mediaPlayerDefaults, currentTime: 0 });
195+
196+
jest.advanceTimersByTime(1000);
197+
session.update(MediaEventType.SeekEnd, { ...mediaPlayerDefaults, currentTime: 7200 });
198+
199+
expect(() => session.toSessionContextEntity()).not.toThrow();
200+
let entity = session.toSessionContextEntity();
201+
// Loop is skipped; only the start second (0) and the landed second (7200) are recorded
202+
expect(entity.contentWatched).toBeLessThanOrEqual(3);
203+
});
204+
205+
it('does not crash when livestream currentTime is a large Unix-epoch offset', () => {
206+
let session = new MediaSessionTrackingStats();
207+
208+
const epochBase = 1_700_000_000;
209+
210+
session.update(MediaEventType.Play, { ...mediaPlayerDefaults, livestream: true, currentTime: epochBase });
211+
212+
jest.advanceTimersByTime(1000);
213+
// Jump larger than MAX_CONTENT_TIME_STEP — simulates stream discontinuity
214+
session.update(MediaEventType.Ping, { ...mediaPlayerDefaults, livestream: true, currentTime: epochBase + 7200 });
215+
216+
expect(() => session.toSessionContextEntity()).not.toThrow();
217+
});
218+
219+
it('still fills intermediate seconds for gaps within MAX_CONTENT_TIME_STEP', () => {
220+
let session = new MediaSessionTrackingStats();
221+
222+
session.update(MediaEventType.Play, { ...mediaPlayerDefaults, currentTime: 0 });
223+
224+
jest.advanceTimersByTime(30 * 1000);
225+
session.update(MediaEventType.End, { ...mediaPlayerDefaults, currentTime: 30 });
226+
227+
let entity = session.toSessionContextEntity();
228+
expect(entity.contentWatched).toBe(31);
229+
});
230+
191231
it('calculates buffering time', () => {
192232
let session = new MediaSessionTrackingStats();
193233

0 commit comments

Comments
 (0)