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

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.b77073f",
3
+ "version": "0.0.0-experiment.b7c103f",
4
4
  "license": "MIT",
5
5
  "author": "Aaron Williams",
6
6
  "main": "dist/es/embeds.js",
@@ -36,11 +36,11 @@
36
36
  },
37
37
  "dependencies": {
38
38
  "@lit/task": "^1.0.0",
39
- "@vouchfor/media-player": "0.0.0-experiment.b77073f",
39
+ "@vouchfor/media-player": "0.0.0-experiment.b7c103f",
40
40
  "uuid": "^9.0.1"
41
41
  },
42
42
  "peerDependencies": {
43
- "lit": "^3.0.2"
43
+ "lit": "^3.1.0"
44
44
  },
45
45
  "devDependencies": {
46
46
  "@esm-bundle/chai": "^4.3.4-fix.0",
@@ -62,7 +62,7 @@
62
62
  "eslint": "^8.50.0",
63
63
  "eslint-plugin-import": "^2.28.1",
64
64
  "lint-staged": "^14.0.1",
65
- "lit": "^2.8.0",
65
+ "lit": "^3.1.0",
66
66
  "prettier": "^3.0.3",
67
67
  "react": "^18.2.0",
68
68
  "react-dom": "^18.2.0",
@@ -39,10 +39,10 @@ type Story = StoryObj<EmbedArgs>;
39
39
 
40
40
  const Embed: Story = {
41
41
  args: {
42
- env: 'dev',
42
+ env: 'local',
43
43
  apiKey: 'TVik9uTMgE-PD25UTHIS6gyl0hMBWC7AT4dkpdlLBT4VIfDWZJrQiCk6Ak7m1',
44
44
  vouchId: '6JQEIPeStt',
45
- templateId: '7d0113f7-3f9a-4bdd-97e3-07ee6eec5730',
45
+ templateId: '357fc118-e179-4171-9446-ff2b8e9d1b29',
46
46
  aspectRatio: 0,
47
47
  preload: 'none',
48
48
  autoplay: false
@@ -50,7 +50,7 @@ const Embed: Story = {
50
50
  argTypes: {
51
51
  env: {
52
52
  control: 'radio',
53
- options: ['prod', 'staging', 'dev']
53
+ options: ['local', 'dev', 'staging', 'prod']
54
54
  },
55
55
  preload: {
56
56
  control: 'radio',
@@ -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';
@@ -31,26 +32,67 @@ class FetcherController {
31
32
  return this._fetching;
32
33
  }
33
34
 
34
- private getVouch = (env: Environment, apiKey: string, vouchId: string) => {
35
+ private getVouch = async (env: Environment, apiKey: string, vouchId: string) => {
35
36
  const { embedApiUrl } = getEnvUrls(env);
36
37
 
37
- return fetch(`${embedApiUrl}/vouches/${vouchId}`, {
38
+ const cacheCheck = uuidv4();
39
+ const res = await fetch(`${embedApiUrl}/vouches/${vouchId}`, {
38
40
  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();
41
+ headers: [
42
+ ['X-Api-Key', apiKey],
43
+ ['X-Cache-Check', cacheCheck]
44
+ ]
43
45
  });
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
+
64
+ return vouch;
44
65
  };
45
66
 
46
- private async getTemplate(env: Environment, apiKey: string, templateId: string) {
67
+ private getTemplate = async (env: Environment, apiKey: string, templateId: string) => {
47
68
  const { embedApiUrl } = getEnvUrls(env);
48
69
 
49
- return fetch(`${embedApiUrl}/templates/${templateId}`, {
70
+ const cacheCheck = uuidv4();
71
+ const res = await fetch(`${embedApiUrl}/templates/${templateId}`, {
50
72
  method: 'GET',
51
- headers: [['X-Api-Key', apiKey]]
52
- }).then((response) => response.json());
53
- }
73
+ headers: [
74
+ ['X-Api-Key', apiKey],
75
+ ['X-Cache-Check', cacheCheck]
76
+ ]
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
+ }
93
+
94
+ return template;
95
+ };
54
96
 
55
97
  constructor(host: EmbedHost) {
56
98
  this.host = host;
@@ -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',
package/src/utils/env.ts CHANGED
@@ -1,15 +1,11 @@
1
- type Environment = 'dev' | 'staging' | 'prod';
1
+ type Environment = 'local' | 'dev' | 'staging' | 'prod';
2
2
 
3
3
  type GetEnvUrlsReturn = {
4
- marketingUrl: string;
5
4
  videoUrl: string;
6
5
  publicApiUrl: string;
7
6
  embedApiUrl: string;
8
- publicRecorderUrl: string;
9
7
  };
10
8
 
11
- const marketingUrl = 'https://vouchfor.com';
12
-
13
9
  const devVideoUrl = 'https://d2rxhdlm2q91uk.cloudfront.net';
14
10
  const stagingVideoUrl = 'https://d1ix11aj5kfygl.cloudfront.net';
15
11
  const prodVideoUrl = 'https://d157jlwnudd93d.cloudfront.net';
@@ -18,61 +14,51 @@ const devPublicApiUrl = 'https://bshyfw4h5a.execute-api.ap-southeast-2.amazonaws
18
14
  const stagingPublicApiUrl = 'https://gyzw7rpbq3.execute-api.ap-southeast-2.amazonaws.com/staging';
19
15
  const prodPublicApiUrl = 'https://vfcjuim1l3.execute-api.ap-southeast-2.amazonaws.com/prod';
20
16
 
17
+ const localEmbedApiUrl = 'http://localhost:6060/v2';
21
18
  const devEmbedApiUrl = 'https://embed-dev.vouchfor.com/v2';
22
19
  const stagingEmbedApiUrl = 'https://embed-staging.vouchfor.com/v2';
23
20
  const prodEmbedApiUrl = 'https://embed.vouchfor.com/v2';
24
21
 
25
- const devPublicRecorderUrl = 'https://dev.vouchfor.com';
26
- const stagingPublicRecorderUrl = 'https://staging.vouchfor.com';
27
- const prodPublicRecorderUrl = 'https://app.vouchfor.com';
28
-
29
22
  // We are handling the case where env is an unknown string so the ts error is a lie
30
23
  // eslint-disable-next-line @typescript-eslint/ban-ts-comment
31
24
  // @ts-ignore
32
25
  function getEnvUrls(env: Environment): GetEnvUrlsReturn {
33
- if (!['dev', 'staging', 'prod'].includes(env)) {
26
+ if (!['local', 'dev', 'staging', 'prod'].includes(env)) {
34
27
  throw new Error(`Unknown environment: ${env}`);
35
28
  }
36
29
 
30
+ if (env === 'local') {
31
+ return {
32
+ videoUrl: devVideoUrl,
33
+ publicApiUrl: devPublicApiUrl,
34
+ embedApiUrl: localEmbedApiUrl
35
+ };
36
+ }
37
+
37
38
  if (env === 'dev') {
38
39
  return {
39
- marketingUrl,
40
40
  videoUrl: devVideoUrl,
41
41
  publicApiUrl: devPublicApiUrl,
42
- embedApiUrl: devEmbedApiUrl,
43
- publicRecorderUrl: devPublicRecorderUrl
42
+ embedApiUrl: devEmbedApiUrl
44
43
  };
45
44
  }
46
45
 
47
46
  if (env === 'staging') {
48
47
  return {
49
- marketingUrl,
50
48
  videoUrl: stagingVideoUrl,
51
49
  publicApiUrl: stagingPublicApiUrl,
52
- embedApiUrl: stagingEmbedApiUrl,
53
- publicRecorderUrl: stagingPublicRecorderUrl
50
+ embedApiUrl: stagingEmbedApiUrl
54
51
  };
55
52
  }
56
53
 
57
54
  if (env === 'prod') {
58
55
  return {
59
- marketingUrl,
60
56
  videoUrl: prodVideoUrl,
61
57
  publicApiUrl: prodPublicApiUrl,
62
- embedApiUrl: prodEmbedApiUrl,
63
- publicRecorderUrl: prodPublicRecorderUrl
58
+ embedApiUrl: prodEmbedApiUrl
64
59
  };
65
60
  }
66
61
  }
67
62
 
68
- export {
69
- marketingUrl,
70
- devEmbedApiUrl,
71
- stagingEmbedApiUrl,
72
- prodEmbedApiUrl,
73
- devPublicRecorderUrl,
74
- stagingPublicRecorderUrl,
75
- prodPublicRecorderUrl,
76
- getEnvUrls
77
- };
63
+ export { devEmbedApiUrl, stagingEmbedApiUrl, prodEmbedApiUrl, getEnvUrls };
78
64
  export type { Environment };
@@ -1,14 +0,0 @@
1
- import type { Embed } from '..';
2
- import type { ReactiveController, ReactiveControllerHost } from 'lit';
3
- type EmbedHost = ReactiveControllerHost & Embed;
4
- declare class EventForwardController implements ReactiveController {
5
- host: EmbedHost;
6
- private _events;
7
- private _cleanup;
8
- private _forwardElementRef;
9
- constructor(host: EmbedHost, events: string[]);
10
- register(): import("lit-html/directive.js").DirectiveResult<typeof import("lit-html/directives/ref.js").RefDirective>;
11
- hostConnected(): void;
12
- hostDisconnected(): void;
13
- }
14
- export { EventForwardController };