forked from snowplow/snowplow-javascript-tracker
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathmediaTracking.ts
More file actions
193 lines (176 loc) · 6.64 KB
/
Copy pathmediaTracking.ts
File metadata and controls
193 lines (176 loc) · 6.64 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
import { resolveDynamicContext, DynamicContext, LOG, SelfDescribingJson } from '@snowplow/tracker-core';
import { MediaAdTracking } from './adTracking';
import { buildMediaPlayerEntity, buildMediaPlayerEvent } from './core';
import { MediaPingInterval } from './pingInterval';
import { MediaSessionTracking } from './sessionTracking';
import {
MediaPlayer,
MediaAdUpdate,
MediaPlayerAdBreakUpdate,
MediaPlayerUpdate,
MediaEventType,
MediaEvent,
EventWithContext,
FilterOutRepeatedEvents,
} from './types';
import { RepeatedEventFilter } from './repeatedEventFilter';
/**
* Manages the state and built-in entities for a media tracking that starts when a user
* call `startMediaTracking` and ends with `endMediaTracking`.
*
* It updates the internal state for each tracked event and returns context entities with
* properties updated based on the internal state.
*/
export class MediaTracking {
/// ID of the media tracking that is used to refer to it by the user.
id: string;
/// Percentage boundaries when to track percent progress events.
private boundaries?: number[];
/// List of boundaries for which percent progress events were already sent to avoid sending again.
private sentBoundaries: number[] = [];
/// State for the media player context entity that is updated as new events are tracked.
player: MediaPlayer = {
currentTime: 0,
paused: true,
ended: false,
};
/// Used to add media player session entity.
private session?: MediaSessionTracking;
/// Tracks ping events independently but stored here to stop when media tracking stops.
private pingInterval?: MediaPingInterval;
/// Manages ad entities.
private adTracking = new MediaAdTracking();
/// Context entities to attach to all events
private customContext?: DynamicContext;
/// Optional list of event types to allow tracking and discard others.
private captureEvents?: MediaEventType[];
// Whether to update page activity when playing media. Enabled by default.
private updatePageActivityWhilePlaying?: boolean;
/// Filters out repeated events based on the configuration.
private repeatedEventFilter: RepeatedEventFilter;
constructor(
id: string,
player?: MediaPlayerUpdate,
session?: MediaSessionTracking,
pingInterval?: MediaPingInterval,
boundaries?: number[],
captureEvents?: MediaEventType[],
updatePageActivityWhilePlaying?: boolean,
filterRepeatedEvents?: FilterOutRepeatedEvents,
context?: DynamicContext
) {
this.id = id;
this.updatePlayer(player);
this.session = session;
this.pingInterval = pingInterval;
this.boundaries = boundaries;
this.captureEvents = captureEvents;
this.updatePageActivityWhilePlaying = updatePageActivityWhilePlaying;
this.customContext = context;
this.repeatedEventFilter = new RepeatedEventFilter(filterRepeatedEvents);
// validate event names in the captureEvents list
captureEvents?.forEach((eventType) => {
if (!Object.values(MediaEventType).includes(eventType)) {
LOG.warn('Unknown media event ' + eventType);
}
});
}
/**
* Called when user calls `endMediaTracking()`.
*/
flushAndStop() {
this.pingInterval?.clear();
this.repeatedEventFilter.flush();
}
/**
* Updates the internal state given the new event or new media player info and returns events to track.
* @param eventType Type of the event tracked or undefined when only updating player properties.
* @param player Updates to the media player stored entity.
* @param ad Updates to the ad entity.
* @param adBreak Updates to the ad break entity.
* @returns List of events with entities to track.
*/
update(
trackEvent: (event: EventWithContext) => void,
mediaEvent?: MediaEvent,
customEvent?: SelfDescribingJson,
player?: MediaPlayerUpdate,
ad?: MediaAdUpdate,
adBreak?: MediaPlayerAdBreakUpdate
) {
// update state
this.updatePlayer(player);
if (mediaEvent !== undefined) {
this.adTracking.updateForThisEvent(mediaEvent.type, this.player, ad, adBreak);
}
this.session?.update(mediaEvent?.type, this.player, this.adTracking.adBreak);
this.pingInterval?.update(this.player);
// build context entities
let context = [buildMediaPlayerEntity(this.player)];
if (this.session !== undefined) {
context.push(this.session.getContext());
}
if (this.customContext) {
context = context.concat(resolveDynamicContext(this.customContext, mediaEvent ?? customEvent));
}
context = context.concat(this.adTracking.getContext());
// build event types to track
const mediaEventsToTrack: MediaEvent[] = [];
if (mediaEvent !== undefined && this.shouldTrackEvent(mediaEvent.type)) {
mediaEventsToTrack.push(mediaEvent);
}
if (this.shouldSendPercentProgress()) {
mediaEventsToTrack.push({
type: MediaEventType.PercentProgress,
eventBody: { percentProgress: this.getPercentProgress() },
});
}
// update state for events after this one
if (mediaEvent !== undefined) {
this.adTracking.updateForNextEvent(mediaEvent.type);
}
const eventsToTrack = mediaEventsToTrack.map((event) => {
return { event: buildMediaPlayerEvent(event), context: context };
});
if (customEvent !== undefined) {
eventsToTrack.push({ event: customEvent, context: context });
}
this.repeatedEventFilter.trackFilteredEvents(eventsToTrack, trackEvent);
}
shouldUpdatePageActivity(): boolean {
return (this.updatePageActivityWhilePlaying ?? true) && !this.player.paused;
}
private updatePlayer(player?: MediaPlayerUpdate) {
if (player !== undefined) {
this.player = {
...this.player,
...player,
};
}
}
private shouldSendPercentProgress(): boolean {
const percentProgress = this.getPercentProgress();
if (this.boundaries === undefined || percentProgress === undefined || this.player.paused) {
return false;
}
let boundaries = this.boundaries.filter((b) => b <= (percentProgress ?? 0));
if (boundaries.length == 0) {
return false;
}
let boundary = Math.max(...boundaries);
if (!this.sentBoundaries.includes(boundary)) {
this.sentBoundaries.push(boundary);
return true;
}
return false;
}
private shouldTrackEvent(eventType: MediaEventType): boolean {
return this.captureEvents === undefined || this.captureEvents.includes(eventType);
}
private getPercentProgress(): number | undefined {
if (this.player.duration === null || this.player.duration === undefined || this.player.duration == 0) {
return undefined;
}
return Math.floor(((this.player.currentTime ?? 0) / this.player.duration) * 100);
}
}