@vouchfor/embeds 0.0.0-experiment.f4df018 → 0.0.0-experiment.f726309

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.f4df018",
3
+ "version": "0.0.0-experiment.f726309",
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.f4df018",
39
+ "@vouchfor/media-player": "0.0.0-experiment.f726309",
40
40
  "uuid": "^9.0.1"
41
41
  },
42
42
  "peerDependencies": {
@@ -22,6 +22,7 @@ const _Embed = ({ vouchId, templateId, preload, autoplay, env, apiKey, controls,
22
22
  ?autoplay=${autoplay}
23
23
  preload=${ifDefined(preload)}
24
24
  aspectRatio=${ifDefined(aspectRatio)}
25
+ @error=${console.log}
25
26
  ></vouch-embed>
26
27
  </div>
27
28
  `;
@@ -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: vouch?.id }));
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
  };
@@ -4,15 +4,16 @@ import type { Embed } from '..';
4
4
  import type { VideoEventDetail } from '@vouchfor/media-player';
5
5
  import type { ReactiveController, ReactiveControllerHost } from 'lit';
6
6
 
7
+ import packageJson from '../../../../package.json';
7
8
  import { getEnvUrls } from '~/utils/env';
8
9
 
9
- const STREAMED_THROTTLE = 2000;
10
+ const MINIMUM_SEND_THRESHOLD = 1;
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
+ vouchId?: string;
16
17
  answerId?: string;
17
18
  streamStart?: number;
18
19
  streamEnd?: number;
@@ -36,21 +37,23 @@ class TrackingController implements ReactiveController {
36
37
  private _hasPlayed = false;
37
38
  private _hasLoaded: BooleanMap = {};
38
39
  private _answersViewed: BooleanMap = {};
39
- private _streamedTime: TimeMap = {};
40
- private _streamedPrevTimestamp: TimeMap = {};
40
+ private _streamStartTime: TimeMap = {};
41
+ private _streamLatestTime: TimeMap = {};
42
+ private _currentlyPlayingVideo: VideoEventDetail | null = null;
41
43
 
42
44
  constructor(host: EmbedHost) {
43
45
  this.host = host;
44
46
  host.addController(this);
45
47
  }
46
48
 
47
- 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
+ private _findVouchId(payload?: TrackingPayload) {
50
+ if (payload && 'vouchId' in payload) {
51
+ return payload.vouchId;
52
+ }
53
+ if (this.host.vouch) {
54
+ return this.host.vouch.id;
53
55
  }
56
+ return null;
54
57
  }
55
58
 
56
59
  private _createVisitor = (visitorId: string) => {
@@ -128,27 +131,56 @@ class TrackingController implements ReactiveController {
128
131
  };
129
132
  };
130
133
 
131
- private _sendTrackingEvent = (event: TrackingEvent, payload: TrackingPayload) => {
134
+ private _sendTrackingEvent = (event: TrackingEvent, payload?: TrackingPayload) => {
135
+ const vouchId = this._findVouchId(payload);
136
+
137
+ if (!vouchId || this.host.disableTracking) {
138
+ return;
139
+ }
140
+
132
141
  const { publicApiUrl } = getEnvUrls(this.host.env);
133
142
  const { client, tab, request, visitor } = this._getUids();
134
143
 
135
- // Don't send tracking if we don't have a source
136
144
  navigator.sendBeacon(
137
- `${publicApiUrl}/api/events`,
145
+ `${publicApiUrl}/api/v2/events`,
138
146
  JSON.stringify({
139
147
  event,
140
- payload,
148
+ payload: {
149
+ ...payload,
150
+ vouchId
151
+ },
141
152
  context: {
142
153
  'x-uid-client': client,
143
154
  'x-uid-tab': tab,
144
155
  'x-uid-request': request,
145
156
  'x-uid-visitor': visitor,
146
- 'x-reporting-metadata': this._getReportingMetadata()
157
+ 'x-reporting-metadata': this._getReportingMetadata(),
158
+ 'x-embeds-version': packageJson.version
147
159
  }
148
160
  })
149
161
  );
150
162
  };
151
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
+
152
184
  private _handleVouchLoaded = ({ detail: vouchId }: CustomEvent<string>) => {
153
185
  if (!vouchId) {
154
186
  return;
@@ -156,91 +188,84 @@ class TrackingController implements ReactiveController {
156
188
 
157
189
  // Only send loaded event once per session
158
190
  if (!this._hasLoaded[vouchId]) {
159
- this._sendTrackingEvent('VOUCH_LOADED', {
160
- vouchId
161
- });
191
+ this._sendTrackingEvent('VOUCH_LOADED', { vouchId });
162
192
  this._hasLoaded[vouchId] = true;
163
193
  }
164
194
  };
165
195
 
166
196
  private _handlePlay = () => {
167
- const vouchId = this._findVouchId();
168
-
169
- if (!vouchId) {
170
- return;
171
- }
172
-
173
197
  // Only send the video played event once per session
174
198
  if (!this._hasPlayed) {
175
199
  this._sendTrackingEvent('VIDEO_PLAYED', {
176
- vouchId,
177
200
  streamStart: this.host.currentTime
178
201
  });
179
202
  this._hasPlayed = true;
180
203
  }
181
204
  };
182
205
 
183
- private _handleVideoPlay = ({ detail: { id, node } }: CustomEvent<VideoEventDetail>) => {
184
- const vouchId = this._findVouchId();
185
- if (!vouchId) {
186
- return;
187
- }
206
+ private _handleVideoPlay = ({ detail: { id, key, node } }: CustomEvent<VideoEventDetail>) => {
188
207
  // Only increment play count once per session
189
- if (!this._answersViewed[id]) {
208
+ if (!this._answersViewed[key]) {
190
209
  this._sendTrackingEvent('VOUCH_RESPONSE_VIEWED', {
191
- vouchId,
192
210
  answerId: id
193
211
  });
194
- this._answersViewed[id] = true;
212
+ this._answersViewed[key] = true;
195
213
  }
196
- this._streamedTime[id] = node.currentTime;
197
- this._streamedPrevTimestamp[id] = Date.now();
198
- };
199
214
 
200
- private _handleVideoTimeUpdate = ({ detail: { id, node } }: CustomEvent<VideoEventDetail>) => {
201
- const vouchId = this._findVouchId();
202
- if (!vouchId) {
203
- return;
215
+ if (!this._streamStartTime[key]) {
216
+ this._streamStartTime[key] = node.currentTime;
217
+ this._streamLatestTime[key] = node.currentTime;
204
218
  }
205
- const currentTimestamp = Date.now();
219
+ };
220
+
221
+ private _handleVideoTimeUpdate = ({ detail: { id, key, node } }: CustomEvent<VideoEventDetail>) => {
206
222
  if (
223
+ // We only want to count any time that the video is actually playing
207
224
  !this.host.paused &&
208
- // Only fire the video seeked event when this video is the active one
209
- id === this.host.scene?.video?.id &&
210
- // Throttle the frequency that we send streamed events while playing
211
- 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
212
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) {
213
235
  this._sendTrackingEvent('VIDEO_STREAMED', {
214
- vouchId,
215
236
  answerId: id,
216
- streamStart: this._streamedTime[id],
217
- streamEnd: node.currentTime
237
+ streamStart: this._streamStartTime[key],
238
+ streamEnd: this._streamLatestTime[key]
218
239
  });
219
- this._streamedTime[id] = node.currentTime;
220
- this._streamedPrevTimestamp[id] = currentTimestamp;
221
240
  }
241
+ delete this._streamStartTime[key];
242
+ delete this._streamLatestTime[key];
222
243
  };
223
244
 
224
- private _handleVideoPause = ({ detail: { id, node } }: CustomEvent<VideoEventDetail>) => {
225
- const vouchId = this._findVouchId();
226
- if (!vouchId) {
227
- return;
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
+ };
251
+
252
+ private _handleVisibilityChange = () => {
253
+ if (document.visibilityState === 'hidden') {
254
+ this._pageUnloading();
228
255
  }
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];
256
+ };
257
+
258
+ private _handlePageHide = () => {
259
+ this._pageUnloading();
240
260
  };
241
261
 
242
262
  hostConnected() {
243
263
  requestAnimationFrame(() => {
264
+ if ('onvisibilitychange' in document) {
265
+ document.addEventListener('visibilitychange', this._handleVisibilityChange);
266
+ } else {
267
+ window.addEventListener('pagehide', this._handlePageHide);
268
+ }
244
269
  this.host.addEventListener('vouch:loaded', this._handleVouchLoaded);
245
270
  this.host.mediaPlayer?.addEventListener('play', this._handlePlay);
246
271
  this.host.mediaPlayer?.addEventListener('video:play', this._handleVideoPlay);
@@ -250,6 +275,12 @@ class TrackingController implements ReactiveController {
250
275
  }
251
276
 
252
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
+ }
253
284
  this.host.removeEventListener('vouch:loaded', this._handleVouchLoaded);
254
285
  this.host.mediaPlayer?.removeEventListener('play', this._handlePlay);
255
286
  this.host.mediaPlayer?.removeEventListener('video:play', this._handleVideoPlay);
@@ -3,7 +3,7 @@ import { customElement, property, state } from 'lit/decorators.js';
3
3
  import { ifDefined } from 'lit/directives/if-defined.js';
4
4
  import { createRef, ref } from 'lit/directives/ref.js';
5
5
 
6
- import type { Scene, TemplateInstance } from '@vouchfor/canvas-video';
6
+ import type { Scene, Scenes, TemplateInstance } from '@vouchfor/canvas-video';
7
7
  import type { MediaPlayer, MediaPlayerProps } from '@vouchfor/media-player';
8
8
  import type { Ref } from 'lit/directives/ref.js';
9
9
  import type { Environment } from '~/utils/env';
@@ -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',
@@ -146,6 +149,10 @@ class Embed extends LitElement {
146
149
  return this._mediaPlayerRef.value?.scenes ?? [];
147
150
  }
148
151
 
152
+ get sceneConfig(): Scenes | null {
153
+ return this._mediaPlayerRef.value?.sceneConfig ?? null;
154
+ }
155
+
149
156
  get videoState() {
150
157
  return this._mediaPlayerRef.value?.videoState;
151
158
  }
File without changes
File without changes
File without changes