@vouchfor/embeds 0.0.0-experiment.af36011 → 0.0.0-experiment.af630d0

Sign up to get free protection for your applications and to get access to all the features.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vouchfor/embeds",
3
- "version": "0.0.0-experiment.af36011",
3
+ "version": "0.0.0-experiment.af630d0",
4
4
  "license": "MIT",
5
5
  "author": "Aaron Williams",
6
6
  "main": "dist/es/embeds.js",
@@ -36,7 +36,7 @@
36
36
  },
37
37
  "dependencies": {
38
38
  "@lit/task": "^1.0.0",
39
- "@vouchfor/media-player": "0.0.0-experiment.af36011",
39
+ "@vouchfor/media-player": "0.0.0-experiment.af630d0",
40
40
  "uuid": "^9.0.1"
41
41
  },
42
42
  "peerDependencies": {
@@ -45,13 +45,13 @@ class FetcherController {
45
45
  });
46
46
 
47
47
  const vouch = await res.json();
48
- this.host.dispatchEvent(new CustomEvent('vouch:loaded', { detail: vouchId }));
48
+ this.host.dispatchEvent(new CustomEvent('vouch:loaded', { detail: vouch?.id }));
49
49
 
50
50
  // HACK: we're currently using API Gateway caching on the embed API without any invalidation logic,
51
51
  // so to ensure that the cache stays up to date, whenever we detect a cache hit we trigger another
52
52
  // API call with the `Cache-Control` header which will re-fill the cache
53
53
  const resCacheCheck = res?.headers?.get('X-Cache-Check');
54
- if (resCacheCheck && resCacheCheck !== cacheCheck) {
54
+ if (resCacheCheck !== cacheCheck) {
55
55
  fetch(`${embedApiUrl}/vouches/${vouchId}`, {
56
56
  method: 'GET',
57
57
  headers: [
@@ -81,7 +81,7 @@ class FetcherController {
81
81
  // so to ensure that the cache stays up to date, whenever we detect a cache hit we trigger another
82
82
  // API call with the `Cache-Control` header which will re-fill the cache
83
83
  const resCacheCheck = res?.headers?.get('X-Cache-Check');
84
- if (resCacheCheck && resCacheCheck !== cacheCheck) {
84
+ if (resCacheCheck !== cacheCheck) {
85
85
  fetch(`${embedApiUrl}/templates/${templateId}`, {
86
86
  method: 'GET',
87
87
  headers: [
@@ -1,19 +1,19 @@
1
- /* eslint-disable max-lines */
2
1
  import { v4 as uuidv4 } from 'uuid';
3
2
 
4
3
  import type { Embed } from '..';
5
4
  import type { VideoEventDetail } from '@vouchfor/media-player';
6
5
  import type { ReactiveController, ReactiveControllerHost } from 'lit';
7
6
 
7
+ import packageJson from '../../../../package.json';
8
8
  import { getEnvUrls } from '~/utils/env';
9
9
 
10
- const STREAMED_THROTTLE = 10000;
10
+ const MINIMUM_SEND_THRESHOLD = 1;
11
11
 
12
12
  type EmbedHost = ReactiveControllerHost & Embed;
13
13
 
14
14
  type TrackingEvent = 'VOUCH_LOADED' | 'VOUCH_RESPONSE_VIEWED' | 'VIDEO_PLAYED' | 'VIDEO_STREAMED';
15
15
  type TrackingPayload = {
16
- vouchId: string;
16
+ vouchId?: string;
17
17
  answerId?: string;
18
18
  streamStart?: number;
19
19
  streamEnd?: number;
@@ -37,22 +37,23 @@ class TrackingController implements ReactiveController {
37
37
  private _hasPlayed = false;
38
38
  private _hasLoaded: BooleanMap = {};
39
39
  private _answersViewed: BooleanMap = {};
40
- private _streamedTime: TimeMap = {};
40
+ private _streamStartTime: TimeMap = {};
41
41
  private _streamLatestTime: TimeMap = {};
42
- private _streamedPrevTimestamp: TimeMap = {};
42
+ private _currentlyPlayingVideo: VideoEventDetail | null = null;
43
43
 
44
44
  constructor(host: EmbedHost) {
45
45
  this.host = host;
46
46
  host.addController(this);
47
47
  }
48
48
 
49
- private _findVouchId() {
49
+ private _findVouchId(payload?: TrackingPayload) {
50
+ if (payload && 'vouchId' in payload) {
51
+ return payload.vouchId;
52
+ }
50
53
  if (this.host.vouch) {
51
- if ('uuid' in this.host.vouch) {
52
- return this.host.vouch.uuid;
53
- }
54
54
  return this.host.vouch.id;
55
55
  }
56
+ return null;
56
57
  }
57
58
 
58
59
  private _createVisitor = (visitorId: string) => {
@@ -130,30 +131,56 @@ class TrackingController implements ReactiveController {
130
131
  };
131
132
  };
132
133
 
133
- private _sendTrackingEvent = (event: TrackingEvent, payload: TrackingPayload) => {
134
- const { publicApiUrl } = getEnvUrls(this.host.env);
135
- const { client, tab, request, visitor } = this._getUids();
134
+ private _sendTrackingEvent = (event: TrackingEvent, payload?: TrackingPayload) => {
135
+ const vouchId = this._findVouchId(payload);
136
136
 
137
- if (this.host.disableTracking) {
137
+ if (!vouchId || this.host.disableTracking) {
138
138
  return;
139
139
  }
140
140
 
141
+ const { publicApiUrl } = getEnvUrls(this.host.env);
142
+ const { client, tab, request, visitor } = this._getUids();
143
+
141
144
  navigator.sendBeacon(
142
- `${publicApiUrl}/api/events`,
145
+ `${publicApiUrl}/api/v2/events`,
143
146
  JSON.stringify({
144
147
  event,
145
- payload,
148
+ payload: {
149
+ ...payload,
150
+ vouchId
151
+ },
146
152
  context: {
147
153
  'x-uid-client': client,
148
154
  'x-uid-tab': tab,
149
155
  'x-uid-request': request,
150
156
  'x-uid-visitor': visitor,
151
- 'x-reporting-metadata': this._getReportingMetadata()
157
+ 'x-reporting-metadata': this._getReportingMetadata(),
158
+ 'x-embeds-version': packageJson.version
152
159
  }
153
160
  })
154
161
  );
155
162
  };
156
163
 
164
+ private _streamEnded = () => {
165
+ if (this._currentlyPlayingVideo) {
166
+ const { id, key } = this._currentlyPlayingVideo;
167
+ // Don't send a tracking event when seeking backwards
168
+ if (this._streamLatestTime[key] > this._streamStartTime[key] + MINIMUM_SEND_THRESHOLD) {
169
+ // Send a video streamed event any time the stream ends to capture the time between starting
170
+ // the video and the video stopping for any reason (pausing, deleting the embed node or closing the browser)
171
+ this._sendTrackingEvent('VIDEO_STREAMED', {
172
+ answerId: id,
173
+ streamStart: this._streamStartTime[key],
174
+ streamEnd: this._streamLatestTime[key]
175
+ });
176
+ }
177
+
178
+ // Make sure these events are only sent once by deleting the start and latest times
179
+ delete this._streamStartTime[key];
180
+ delete this._streamLatestTime[key];
181
+ }
182
+ };
183
+
157
184
  private _handleVouchLoaded = ({ detail: vouchId }: CustomEvent<string>) => {
158
185
  if (!vouchId) {
159
186
  return;
@@ -161,143 +188,102 @@ class TrackingController implements ReactiveController {
161
188
 
162
189
  // Only send loaded event once per session
163
190
  if (!this._hasLoaded[vouchId]) {
164
- this._sendTrackingEvent('VOUCH_LOADED', {
165
- vouchId
166
- });
191
+ this._sendTrackingEvent('VOUCH_LOADED', { vouchId });
167
192
  this._hasLoaded[vouchId] = true;
168
193
  }
169
194
  };
170
195
 
171
196
  private _handlePlay = () => {
172
- const vouchId = this._findVouchId();
173
-
174
- if (!vouchId) {
175
- return;
176
- }
177
-
178
197
  // Only send the video played event once per session
179
198
  if (!this._hasPlayed) {
180
199
  this._sendTrackingEvent('VIDEO_PLAYED', {
181
- vouchId,
182
200
  streamStart: this.host.currentTime
183
201
  });
184
202
  this._hasPlayed = true;
185
203
  }
186
204
  };
187
205
 
188
- private _handleVideoPlay = ({ detail: { id, node } }: CustomEvent<VideoEventDetail>) => {
189
- const vouchId = this._findVouchId();
190
-
191
- if (!vouchId) {
192
- return;
193
- }
194
-
206
+ private _handleVideoPlay = ({ detail: { id, key, node } }: CustomEvent<VideoEventDetail>) => {
195
207
  // Only increment play count once per session
196
- if (!this._answersViewed[id]) {
208
+ if (!this._answersViewed[key]) {
197
209
  this._sendTrackingEvent('VOUCH_RESPONSE_VIEWED', {
198
- vouchId,
199
210
  answerId: id
200
211
  });
201
- this._answersViewed[id] = true;
202
- }
203
-
204
- this._streamedTime[id] = node.currentTime;
205
- this._streamLatestTime[id] = node.currentTime;
206
- this._streamedPrevTimestamp[id] = Date.now();
207
- };
208
-
209
- private _handleVideoSeeking = ({ detail: { id } }: CustomEvent<VideoEventDetail>) => {
210
- const vouchId = this._findVouchId();
211
-
212
- if (!vouchId) {
213
- return;
212
+ this._answersViewed[key] = true;
214
213
  }
215
214
 
216
- if (this._streamLatestTime[id]) {
217
- this._sendTrackingEvent('VIDEO_STREAMED', {
218
- vouchId,
219
- answerId: id,
220
- streamStart: this._streamedTime[id],
221
- streamEnd: this._streamLatestTime[id]
222
- });
215
+ if (!this._streamStartTime[key]) {
216
+ this._streamStartTime[key] = node.currentTime;
217
+ this._streamLatestTime[key] = node.currentTime;
223
218
  }
224
-
225
- delete this._streamedTime[id];
226
- delete this._streamLatestTime[id];
227
- delete this._streamedPrevTimestamp[id];
228
219
  };
229
220
 
230
- private _handleVideoTimeUpdate = ({ detail: { id, node } }: CustomEvent<VideoEventDetail>) => {
231
- const vouchId = this._findVouchId();
232
-
233
- if (!vouchId) {
234
- return;
235
- }
236
-
237
- const currentTimestamp = Date.now();
221
+ private _handleVideoTimeUpdate = ({ detail: { id, key, node } }: CustomEvent<VideoEventDetail>) => {
238
222
  if (
239
- node.currentTime &&
240
- !node.paused &&
223
+ // We only want to count any time that the video is actually playing
241
224
  !this.host.paused &&
242
- // Only fire the video seeked event when this video is the active one
243
- id === this.host.scene?.video?.id &&
244
- // Throttle the frequency that we send streamed events while playing
245
- currentTimestamp - this._streamedPrevTimestamp[id] > STREAMED_THROTTLE
225
+ // Only update the latest time if this event fires for the currently active video
226
+ id === this.host.scene?.video?.id
246
227
  ) {
228
+ this._currentlyPlayingVideo = { id, key, node };
229
+ this._streamLatestTime[key] = node.currentTime;
230
+ }
231
+ };
232
+
233
+ private _handleVideoPause = ({ detail: { id, key } }: CustomEvent<VideoEventDetail>) => {
234
+ if (this._streamLatestTime[key] > this._streamStartTime[key] + MINIMUM_SEND_THRESHOLD) {
247
235
  this._sendTrackingEvent('VIDEO_STREAMED', {
248
- vouchId,
249
236
  answerId: id,
250
- streamStart: this._streamedTime[id],
251
- streamEnd: node.currentTime
237
+ streamStart: this._streamStartTime[key],
238
+ streamEnd: this._streamLatestTime[key]
252
239
  });
253
- this._streamedTime[id] = node.currentTime;
254
- this._streamedPrevTimestamp[id] = currentTimestamp;
255
240
  }
256
-
257
- this._streamLatestTime[id] = node.currentTime;
241
+ delete this._streamStartTime[key];
242
+ delete this._streamLatestTime[key];
258
243
  };
259
244
 
260
- private _handleVideoPause = ({ detail: { id, node } }: CustomEvent<VideoEventDetail>) => {
261
- const vouchId = this._findVouchId();
245
+ private _pageUnloading = () => {
246
+ this._streamEnded();
247
+ // This will try to send the same stream event again so we delete the start and latest
248
+ // time in stream ended so that there is no times to send and the pause event does nothing
249
+ this.host.pause();
250
+ };
262
251
 
263
- if (!vouchId) {
264
- return;
265
- }
266
-
267
- // Don't send a tracking event if the video pauses when seeking backwards
268
- if (node.currentTime > this._streamedTime[id]) {
269
- // Send a video streamed event any time the video pauses then reset the streamed state
270
- // We do this to capture the last bit of time that the video was played between the previous
271
- // stream event and the video being paused manually or stopping because it ended
272
- this._sendTrackingEvent('VIDEO_STREAMED', {
273
- vouchId,
274
- answerId: id,
275
- streamStart: this._streamedTime[id],
276
- streamEnd: node.currentTime
277
- });
252
+ private _handleVisibilityChange = () => {
253
+ if (document.visibilityState === 'hidden') {
254
+ this._pageUnloading();
278
255
  }
256
+ };
279
257
 
280
- delete this._streamedTime[id];
281
- delete this._streamLatestTime[id];
282
- delete this._streamedPrevTimestamp[id];
258
+ private _handlePageHide = () => {
259
+ this._pageUnloading();
283
260
  };
284
261
 
285
262
  hostConnected() {
286
263
  requestAnimationFrame(() => {
264
+ if ('onvisibilitychange' in document) {
265
+ document.addEventListener('visibilitychange', this._handleVisibilityChange);
266
+ } else {
267
+ window.addEventListener('pagehide', this._handlePageHide);
268
+ }
287
269
  this.host.addEventListener('vouch:loaded', this._handleVouchLoaded);
288
270
  this.host.mediaPlayer?.addEventListener('play', this._handlePlay);
289
271
  this.host.mediaPlayer?.addEventListener('video:play', this._handleVideoPlay);
290
- this.host.mediaPlayer?.addEventListener('video:seeking', this._handleVideoSeeking);
291
272
  this.host.mediaPlayer?.addEventListener('video:pause', this._handleVideoPause);
292
273
  this.host.mediaPlayer?.addEventListener('video:timeupdate', this._handleVideoTimeUpdate);
293
274
  });
294
275
  }
295
276
 
296
277
  hostDisconnected() {
278
+ this._streamEnded();
279
+ if ('onvisibilitychange' in document) {
280
+ document.removeEventListener('visibilitychange', this._handleVisibilityChange);
281
+ } else {
282
+ window.removeEventListener('pagehide', this._handlePageHide);
283
+ }
297
284
  this.host.removeEventListener('vouch:loaded', this._handleVouchLoaded);
298
285
  this.host.mediaPlayer?.removeEventListener('play', this._handlePlay);
299
286
  this.host.mediaPlayer?.removeEventListener('video:play', this._handleVideoPlay);
300
- this.host.mediaPlayer?.removeEventListener('video:seeking', this._handleVideoSeeking);
301
287
  this.host.mediaPlayer?.removeEventListener('video:pause', this._handleVideoPause);
302
288
  this.host.mediaPlayer?.removeEventListener('video:timeupdate', this._handleVideoTimeUpdate);
303
289
  }
File without changes
File without changes
File without changes