@vouchfor/embeds 0.0.0-experiment.658bac6 → 0.0.0-experiment.672c2a0

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.658bac6",
3
+ "version": "0.0.0-experiment.672c2a0",
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.658bac6",
39
+ "@vouchfor/media-player": "0.0.0-experiment.672c2a0",
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 !== 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 !== 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
  };
@@ -6,13 +6,13 @@ import type { ReactiveController, ReactiveControllerHost } from 'lit';
6
6
 
7
7
  import { getEnvUrls } from '~/utils/env';
8
8
 
9
- const STREAMED_THROTTLE = 2000;
9
+ // In seconds due to checking against node.currentTime
10
+ const STREAMED_THROTTLE = 10;
10
11
 
11
12
  type EmbedHost = ReactiveControllerHost & Embed;
12
13
 
13
14
  type TrackingEvent = 'VOUCH_LOADED' | 'VOUCH_RESPONSE_VIEWED' | 'VIDEO_PLAYED' | 'VIDEO_STREAMED';
14
15
  type TrackingPayload = {
15
- vouchId: string;
16
16
  answerId?: string;
17
17
  streamStart?: number;
18
18
  streamEnd?: number;
@@ -36,8 +36,9 @@ class TrackingController implements ReactiveController {
36
36
  private _hasPlayed = false;
37
37
  private _hasLoaded: BooleanMap = {};
38
38
  private _answersViewed: BooleanMap = {};
39
- private _streamedTime: TimeMap = {};
40
- private _streamedPrevTimestamp: TimeMap = {};
39
+ private _streamStartTime: TimeMap = {};
40
+ private _streamLatestTime: TimeMap = {};
41
+ private _currentlyPlayingVideo: VideoEventDetail | null = null;
41
42
 
42
43
  constructor(host: EmbedHost) {
43
44
  this.host = host;
@@ -45,11 +46,11 @@ class TrackingController implements ReactiveController {
45
46
  }
46
47
 
47
48
  private _findVouchId() {
48
- if (this.host.data) {
49
- if ('uuid' in this.host.data) {
50
- return this.host.data.uuid;
49
+ if (this.host.vouch) {
50
+ if ('uuid' in this.host.vouch) {
51
+ return this.host.vouch.uuid;
51
52
  }
52
- return this.host.data.id;
53
+ return this.host.vouch.id;
53
54
  }
54
55
  }
55
56
 
@@ -128,11 +129,16 @@ class TrackingController implements ReactiveController {
128
129
  };
129
130
  };
130
131
 
131
- private _sendTrackingEvent = (event: TrackingEvent, payload: TrackingPayload) => {
132
+ private _sendTrackingEvent = (event: TrackingEvent, payload?: TrackingPayload) => {
133
+ const vouchId = this._findVouchId();
134
+
135
+ if (!vouchId || this.host.disableTracking) {
136
+ return;
137
+ }
138
+
132
139
  const { publicApiUrl } = getEnvUrls(this.host.env);
133
140
  const { client, tab, request, visitor } = this._getUids();
134
141
 
135
- // Don't send tracking if we don't have a source
136
142
  navigator.sendBeacon(
137
143
  `${publicApiUrl}/api/events`,
138
144
  JSON.stringify({
@@ -156,87 +162,74 @@ class TrackingController implements ReactiveController {
156
162
 
157
163
  // Only send loaded event once per session
158
164
  if (!this._hasLoaded[vouchId]) {
159
- this._sendTrackingEvent('VOUCH_LOADED', {
160
- vouchId
161
- });
165
+ this._sendTrackingEvent('VOUCH_LOADED');
162
166
  this._hasLoaded[vouchId] = true;
163
167
  }
164
168
  };
165
169
 
166
170
  private _handlePlay = () => {
167
- const vouchId = this._findVouchId();
168
-
169
- if (!vouchId) {
170
- return;
171
- }
172
-
173
171
  // Only send the video played event once per session
174
172
  if (!this._hasPlayed) {
175
173
  this._sendTrackingEvent('VIDEO_PLAYED', {
176
- vouchId,
177
174
  streamStart: this.host.currentTime
178
175
  });
179
176
  this._hasPlayed = true;
180
177
  }
181
178
  };
182
179
 
183
- private _handleVideoPlay = ({ detail: { id, node } }: CustomEvent<VideoEventDetail>) => {
184
- const vouchId = this._findVouchId();
185
- if (!vouchId) {
186
- return;
187
- }
180
+ private _handleVideoPlay = ({ detail: { id, key, node } }: CustomEvent<VideoEventDetail>) => {
188
181
  // Only increment play count once per session
189
- if (!this._answersViewed[id]) {
182
+ if (!this._answersViewed[key]) {
190
183
  this._sendTrackingEvent('VOUCH_RESPONSE_VIEWED', {
191
- vouchId,
192
184
  answerId: id
193
185
  });
194
- this._answersViewed[id] = true;
186
+ this._answersViewed[key] = true;
195
187
  }
196
- this._streamedTime[id] = node.currentTime;
197
- this._streamedPrevTimestamp[id] = Date.now();
188
+
189
+ this._streamStartTime[key] = node.currentTime;
190
+ this._streamLatestTime[key] = node.currentTime;
198
191
  };
199
192
 
200
- private _handleVideoTimeUpdate = ({ detail: { id, node } }: CustomEvent<VideoEventDetail>) => {
201
- const vouchId = this._findVouchId();
202
- if (!vouchId) {
203
- return;
193
+ private _handleVideoTimeUpdate = ({ detail: { id, key, node } }: CustomEvent<VideoEventDetail>) => {
194
+ // We only want to count any time that the video is actually playing
195
+ if (!this.host.paused) {
196
+ this._currentlyPlayingVideo = { id, key, node };
197
+ this._streamLatestTime[key] = node.currentTime;
204
198
  }
205
- const currentTimestamp = Date.now();
199
+
206
200
  if (
201
+ !node.paused &&
207
202
  !this.host.paused &&
208
203
  // Only fire the video seeked event when this video is the active one
209
204
  id === this.host.scene?.video?.id &&
210
205
  // Throttle the frequency that we send streamed events while playing
211
- currentTimestamp - this._streamedPrevTimestamp[id] > STREAMED_THROTTLE
206
+ this._streamLatestTime[key] - this._streamStartTime[key] > STREAMED_THROTTLE
212
207
  ) {
213
208
  this._sendTrackingEvent('VIDEO_STREAMED', {
214
- vouchId,
215
209
  answerId: id,
216
- streamStart: this._streamedTime[id],
217
- streamEnd: node.currentTime
210
+ streamStart: this._streamStartTime[key],
211
+ streamEnd: this._streamLatestTime[key]
218
212
  });
219
- this._streamedTime[id] = node.currentTime;
220
- this._streamedPrevTimestamp[id] = currentTimestamp;
213
+
214
+ this._streamStartTime[key] = node.currentTime;
221
215
  }
222
216
  };
223
217
 
224
- private _handleVideoPause = ({ detail: { id, node } }: CustomEvent<VideoEventDetail>) => {
225
- const vouchId = this._findVouchId();
226
- if (!vouchId) {
227
- return;
218
+ private _handleVideoPause = ({ detail: { id, key } }: CustomEvent<VideoEventDetail>) => {
219
+ // Don't send a tracking event when seeking backwards
220
+ if (this._streamLatestTime[key] > this._streamStartTime[key]) {
221
+ // Send a video streamed event any time the video pauses then reset the streamed state
222
+ // We do this to capture the last bit of time that the video was played between the previous
223
+ // stream event and the video being paused manually or stopping because it ended
224
+ this._sendTrackingEvent('VIDEO_STREAMED', {
225
+ answerId: id,
226
+ streamStart: this._streamStartTime[key],
227
+ streamEnd: this._streamLatestTime[key]
228
+ });
228
229
  }
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];
230
+ this._currentlyPlayingVideo = null;
231
+ delete this._streamStartTime[key];
232
+ delete this._streamLatestTime[key];
240
233
  };
241
234
 
242
235
  hostConnected() {
@@ -250,6 +243,20 @@ class TrackingController implements ReactiveController {
250
243
  }
251
244
 
252
245
  hostDisconnected() {
246
+ if (this._currentlyPlayingVideo) {
247
+ const { id, key } = this._currentlyPlayingVideo;
248
+ if (this._streamLatestTime[key] > this._streamStartTime[key]) {
249
+ // Send a video streamed event any time the video pauses then reset the streamed state
250
+ // We do this to capture the last bit of time that the video was played between the previous
251
+ // stream event and the video being paused manually or stopping because it ended
252
+ this._sendTrackingEvent('VIDEO_STREAMED', {
253
+ answerId: id,
254
+ streamStart: this._streamStartTime[key],
255
+ streamEnd: this._streamLatestTime[key]
256
+ });
257
+ }
258
+ }
259
+
253
260
  this.host.removeEventListener('vouch:loaded', this._handleVouchLoaded);
254
261
  this.host.mediaPlayer?.removeEventListener('play', this._handlePlay);
255
262
  this.host.mediaPlayer?.removeEventListener('video:play', this._handleVideoPlay);
@@ -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',