@vouchfor/embeds 0.0.0-experiment.b78d535 → 0.0.0-experiment.b7c103f

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vouchfor/embeds",
3
- "version": "0.0.0-experiment.b78d535",
3
+ "version": "0.0.0-experiment.b7c103f",
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.b78d535",
39
+ "@vouchfor/media-player": "0.0.0-experiment.b7c103f",
40
40
  "uuid": "^9.0.1"
41
41
  },
42
42
  "peerDependencies": {
@@ -1,4 +1,5 @@
1
1
  import { Task } from '@lit/task';
2
+ import { v4 as uuidv4 } from 'uuid';
2
3
 
3
4
  import type { Embed, EmbedProps } from '..';
4
5
  import type { ReactiveControllerHost } from 'lit';
@@ -34,42 +35,61 @@ class FetcherController {
34
35
  private getVouch = async (env: Environment, apiKey: string, vouchId: string) => {
35
36
  const { embedApiUrl } = getEnvUrls(env);
36
37
 
37
- const vouch = await fetch(`${embedApiUrl}/vouches/${vouchId}`, {
38
- method: 'GET',
39
- headers: [['X-Api-Key', apiKey]]
40
- }).then((response) => {
41
- this.host.dispatchEvent(new CustomEvent('vouch:loaded', { detail: vouchId }));
42
- return response.json();
43
- });
44
-
45
- // HACK: trigger another fetch after we received the data to update the cache in the background
46
- fetch(`${embedApiUrl}/vouches/${vouchId}`, {
38
+ const cacheCheck = uuidv4();
39
+ const res = await fetch(`${embedApiUrl}/vouches/${vouchId}`, {
47
40
  method: 'GET',
48
41
  headers: [
49
42
  ['X-Api-Key', apiKey],
50
- ['Cache-Control', 'max-age=0']
43
+ ['X-Cache-Check', cacheCheck]
51
44
  ]
52
45
  });
53
46
 
47
+ const vouch = await res.json();
48
+ this.host.dispatchEvent(new CustomEvent('vouch:loaded', { detail: vouchId }));
49
+
50
+ // HACK: we're currently using API Gateway caching on the embed API without any invalidation logic,
51
+ // so to ensure that the cache stays up to date, whenever we detect a cache hit we trigger another
52
+ // API call with the `Cache-Control` header which will re-fill the cache
53
+ const resCacheCheck = res?.headers?.get('X-Cache-Check');
54
+ if (resCacheCheck && resCacheCheck !== cacheCheck) {
55
+ fetch(`${embedApiUrl}/vouches/${vouchId}`, {
56
+ method: 'GET',
57
+ headers: [
58
+ ['X-Api-Key', apiKey],
59
+ ['Cache-Control', 'max-age=0']
60
+ ]
61
+ });
62
+ }
63
+
54
64
  return vouch;
55
65
  };
56
66
 
57
67
  private getTemplate = async (env: Environment, apiKey: string, templateId: string) => {
58
68
  const { embedApiUrl } = getEnvUrls(env);
59
69
 
60
- const template = await fetch(`${embedApiUrl}/templates/${templateId}`, {
61
- method: 'GET',
62
- headers: [['X-Api-Key', apiKey]]
63
- }).then((response) => response.json());
64
-
65
- // HACK: trigger another fetch after we received the data to update the cache in the background
66
- fetch(`${embedApiUrl}/templates/${templateId}`, {
70
+ const cacheCheck = uuidv4();
71
+ const res = await fetch(`${embedApiUrl}/templates/${templateId}`, {
67
72
  method: 'GET',
68
73
  headers: [
69
74
  ['X-Api-Key', apiKey],
70
- ['Cache-Control', 'max-age=0']
75
+ ['X-Cache-Check', cacheCheck]
71
76
  ]
72
77
  });
78
+ const template = await res.json();
79
+
80
+ // HACK: we're currently using API Gateway caching on the embed API without any invalidation logic,
81
+ // so to ensure that the cache stays up to date, whenever we detect a cache hit we trigger another
82
+ // API call with the `Cache-Control` header which will re-fill the cache
83
+ const resCacheCheck = res?.headers?.get('X-Cache-Check');
84
+ if (resCacheCheck && resCacheCheck !== cacheCheck) {
85
+ fetch(`${embedApiUrl}/templates/${templateId}`, {
86
+ method: 'GET',
87
+ headers: [
88
+ ['X-Api-Key', apiKey],
89
+ ['Cache-Control', 'max-age=0']
90
+ ]
91
+ });
92
+ }
73
93
 
74
94
  return template;
75
95
  };
@@ -1,3 +1,4 @@
1
+ /* eslint-disable max-lines */
1
2
  import { v4 as uuidv4 } from 'uuid';
2
3
 
3
4
  import type { Embed } from '..';
@@ -6,7 +7,7 @@ import type { ReactiveController, ReactiveControllerHost } from 'lit';
6
7
 
7
8
  import { getEnvUrls } from '~/utils/env';
8
9
 
9
- const STREAMED_THROTTLE = 2000;
10
+ const STREAMED_THROTTLE = 10000;
10
11
 
11
12
  type EmbedHost = ReactiveControllerHost & Embed;
12
13
 
@@ -37,6 +38,7 @@ class TrackingController implements ReactiveController {
37
38
  private _hasLoaded: BooleanMap = {};
38
39
  private _answersViewed: BooleanMap = {};
39
40
  private _streamedTime: TimeMap = {};
41
+ private _streamLatestTime: TimeMap = {};
40
42
  private _streamedPrevTimestamp: TimeMap = {};
41
43
 
42
44
  constructor(host: EmbedHost) {
@@ -45,11 +47,11 @@ class TrackingController implements ReactiveController {
45
47
  }
46
48
 
47
49
  private _findVouchId() {
48
- if (this.host.data) {
49
- if ('uuid' in this.host.data) {
50
- return this.host.data.uuid;
50
+ if (this.host.vouch) {
51
+ if ('uuid' in this.host.vouch) {
52
+ return this.host.vouch.uuid;
51
53
  }
52
- return this.host.data.id;
54
+ return this.host.vouch.id;
53
55
  }
54
56
  }
55
57
 
@@ -132,7 +134,10 @@ class TrackingController implements ReactiveController {
132
134
  const { publicApiUrl } = getEnvUrls(this.host.env);
133
135
  const { client, tab, request, visitor } = this._getUids();
134
136
 
135
- // Don't send tracking if we don't have a source
137
+ if (this.host.disableTracking) {
138
+ return;
139
+ }
140
+
136
141
  navigator.sendBeacon(
137
142
  `${publicApiUrl}/api/events`,
138
143
  JSON.stringify({
@@ -180,63 +185,101 @@ class TrackingController implements ReactiveController {
180
185
  }
181
186
  };
182
187
 
183
- private _handleVideoPlay = ({ detail: { id, node } }: CustomEvent<VideoEventDetail>) => {
188
+ private _handleVideoPlay = ({ detail: { id, key, node } }: CustomEvent<VideoEventDetail>) => {
184
189
  const vouchId = this._findVouchId();
190
+
185
191
  if (!vouchId) {
186
192
  return;
187
193
  }
194
+
188
195
  // Only increment play count once per session
189
- if (!this._answersViewed[id]) {
196
+ if (!this._answersViewed[key]) {
190
197
  this._sendTrackingEvent('VOUCH_RESPONSE_VIEWED', {
191
198
  vouchId,
192
199
  answerId: id
193
200
  });
194
- this._answersViewed[id] = true;
201
+ this._answersViewed[key] = true;
195
202
  }
196
- this._streamedTime[id] = node.currentTime;
197
- this._streamedPrevTimestamp[id] = Date.now();
203
+
204
+ this._streamedTime[key] = node.currentTime;
205
+ this._streamLatestTime[key] = node.currentTime;
206
+ this._streamedPrevTimestamp[key] = Date.now();
198
207
  };
199
208
 
200
- private _handleVideoTimeUpdate = ({ detail: { id, node } }: CustomEvent<VideoEventDetail>) => {
209
+ private _handleVideoSeeking = ({ detail: { id, key } }: CustomEvent<VideoEventDetail>) => {
201
210
  const vouchId = this._findVouchId();
211
+
202
212
  if (!vouchId) {
203
213
  return;
204
214
  }
215
+
216
+ if (this._streamLatestTime[key]) {
217
+ this._sendTrackingEvent('VIDEO_STREAMED', {
218
+ vouchId,
219
+ answerId: id,
220
+ streamStart: this._streamedTime[key],
221
+ streamEnd: this._streamLatestTime[key]
222
+ });
223
+ }
224
+
225
+ delete this._streamedTime[key];
226
+ delete this._streamLatestTime[key];
227
+ delete this._streamedPrevTimestamp[key];
228
+ };
229
+
230
+ private _handleVideoTimeUpdate = ({ detail: { id, key, node } }: CustomEvent<VideoEventDetail>) => {
231
+ const vouchId = this._findVouchId();
232
+
233
+ if (!vouchId) {
234
+ return;
235
+ }
236
+
205
237
  const currentTimestamp = Date.now();
206
238
  if (
239
+ node.currentTime &&
240
+ !node.paused &&
207
241
  !this.host.paused &&
208
242
  // Only fire the video seeked event when this video is the active one
209
243
  id === this.host.scene?.video?.id &&
210
244
  // Throttle the frequency that we send streamed events while playing
211
- currentTimestamp - this._streamedPrevTimestamp[id] > STREAMED_THROTTLE
245
+ currentTimestamp - this._streamedPrevTimestamp[key] > STREAMED_THROTTLE
212
246
  ) {
213
247
  this._sendTrackingEvent('VIDEO_STREAMED', {
214
248
  vouchId,
215
249
  answerId: id,
216
- streamStart: this._streamedTime[id],
250
+ streamStart: this._streamedTime[key],
217
251
  streamEnd: node.currentTime
218
252
  });
219
- this._streamedTime[id] = node.currentTime;
220
- this._streamedPrevTimestamp[id] = currentTimestamp;
253
+ this._streamedTime[key] = node.currentTime;
254
+ this._streamedPrevTimestamp[key] = currentTimestamp;
221
255
  }
256
+
257
+ this._streamLatestTime[key] = node.currentTime;
222
258
  };
223
259
 
224
- private _handleVideoPause = ({ detail: { id, node } }: CustomEvent<VideoEventDetail>) => {
260
+ private _handleVideoPause = ({ detail: { id, key, node } }: CustomEvent<VideoEventDetail>) => {
225
261
  const vouchId = this._findVouchId();
262
+
226
263
  if (!vouchId) {
227
264
  return;
228
265
  }
229
- // Send a video streamed event any time the video pauses then reset the streamed state
230
- // We do this to capture the last bit of time that the video was played between the previous
231
- // stream event and the video being paused manually or stopping because it ended
232
- this._sendTrackingEvent('VIDEO_STREAMED', {
233
- vouchId,
234
- answerId: id,
235
- streamStart: this._streamedTime[id],
236
- streamEnd: node.currentTime
237
- });
238
- delete this._streamedTime[id];
239
- delete this._streamedPrevTimestamp[id];
266
+
267
+ // Don't send a tracking event if the video pauses when seeking backwards
268
+ if (node.currentTime > this._streamedTime[key]) {
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[key],
276
+ streamEnd: node.currentTime
277
+ });
278
+ }
279
+
280
+ delete this._streamedTime[key];
281
+ delete this._streamLatestTime[key];
282
+ delete this._streamedPrevTimestamp[key];
240
283
  };
241
284
 
242
285
  hostConnected() {
@@ -244,6 +287,7 @@ class TrackingController implements ReactiveController {
244
287
  this.host.addEventListener('vouch:loaded', this._handleVouchLoaded);
245
288
  this.host.mediaPlayer?.addEventListener('play', this._handlePlay);
246
289
  this.host.mediaPlayer?.addEventListener('video:play', this._handleVideoPlay);
290
+ this.host.mediaPlayer?.addEventListener('video:seeking', this._handleVideoSeeking);
247
291
  this.host.mediaPlayer?.addEventListener('video:pause', this._handleVideoPause);
248
292
  this.host.mediaPlayer?.addEventListener('video:timeupdate', this._handleVideoTimeUpdate);
249
293
  });
@@ -253,6 +297,7 @@ class TrackingController implements ReactiveController {
253
297
  this.host.removeEventListener('vouch:loaded', this._handleVouchLoaded);
254
298
  this.host.mediaPlayer?.removeEventListener('play', this._handlePlay);
255
299
  this.host.mediaPlayer?.removeEventListener('video:play', this._handleVideoPlay);
300
+ this.host.mediaPlayer?.removeEventListener('video:seeking', this._handleVideoSeeking);
256
301
  this.host.mediaPlayer?.removeEventListener('video:pause', this._handleVideoPause);
257
302
  this.host.mediaPlayer?.removeEventListener('video:timeupdate', this._handleVideoTimeUpdate);
258
303
  }
@@ -17,6 +17,7 @@ import '@vouchfor/media-player';
17
17
  type EmbedProps = Pick<MediaPlayerProps, 'data' | 'aspectRatio' | 'preload' | 'autoplay' | 'controls'> & {
18
18
  env: Environment;
19
19
  apiKey: string;
20
+ disableTracking?: boolean;
20
21
  trackingSource?: string;
21
22
  vouchId?: string;
22
23
  templateId?: string;
@@ -32,6 +33,7 @@ class Embed extends LitElement {
32
33
 
33
34
  @property({ type: String }) env: EmbedProps['env'] = 'prod';
34
35
  @property({ type: String }) apiKey: EmbedProps['apiKey'] = '';
36
+ @property({ type: Boolean }) disableTracking: EmbedProps['disableTracking'] = false;
35
37
  @property({ type: String }) trackingSource: EmbedProps['trackingSource'] = 'embed';
36
38
 
37
39
  @property({ type: Array }) controls: EmbedProps['controls'];
@@ -57,6 +59,7 @@ class Embed extends LitElement {
57
59
  'waiting',
58
60
 
59
61
  'video:loadeddata',
62
+ 'video:seeking',
60
63
  'video:seeked',
61
64
  'video:play',
62
65
  'video:playing',