@vouchfor/embeds 0.0.0-experiment.5e5848e → 0.0.0-experiment.607fdcd

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