@vouchfor/embeds 0.0.0-experiment.93ea548 → 0.0.0-experiment.9e95139

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.93ea548",
3
+ "version": "0.0.0-experiment.9e95139",
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.93ea548",
39
+ "@vouchfor/media-player": "0.0.0-experiment.9e95139",
40
40
  "uuid": "^9.0.1"
41
41
  },
42
42
  "peerDependencies": {
@@ -10,7 +10,17 @@ type EmbedArgs = EmbedProps & {
10
10
  showVouch?: boolean;
11
11
  };
12
12
 
13
- const _Embed = ({ vouchId, templateId, preload, autoplay, env, apiKey, controls, aspectRatio }: EmbedArgs) => {
13
+ const _Embed = ({
14
+ vouchId,
15
+ templateId,
16
+ questionIndexes,
17
+ preload,
18
+ autoplay,
19
+ env,
20
+ apiKey,
21
+ controls,
22
+ aspectRatio
23
+ }: EmbedArgs) => {
14
24
  return html`
15
25
  <div style="height: 100vh">
16
26
  <vouch-embed
@@ -18,10 +28,12 @@ const _Embed = ({ vouchId, templateId, preload, autoplay, env, apiKey, controls,
18
28
  apiKey=${ifDefined(apiKey)}
19
29
  vouchId=${ifDefined(vouchId)}
20
30
  templateId=${ifDefined(templateId)}
31
+ .questionIndexes=${questionIndexes}
21
32
  .controls=${controls}
22
33
  ?autoplay=${autoplay}
23
34
  preload=${ifDefined(preload)}
24
35
  aspectRatio=${ifDefined(aspectRatio)}
36
+ @error=${console.log}
25
37
  ></vouch-embed>
26
38
  </div>
27
39
  `;
@@ -43,6 +55,7 @@ const Embed: Story = {
43
55
  apiKey: 'TVik9uTMgE-PD25UTHIS6gyl0hMBWC7AT4dkpdlLBT4VIfDWZJrQiCk6Ak7m1',
44
56
  vouchId: '6JQEIPeStt',
45
57
  templateId: '357fc118-e179-4171-9446-ff2b8e9d1b29',
58
+ questionIndexes: [],
46
59
  aspectRatio: 0,
47
60
  preload: 'none',
48
61
  autoplay: false
@@ -9,7 +9,7 @@ import { getEnvUrls } from '~/utils/env';
9
9
 
10
10
  type EmbedHost = ReactiveControllerHost & Embed;
11
11
 
12
- type TaskDeps = [
12
+ type FetchTaskDeps = [
13
13
  EmbedProps['env'],
14
14
  EmbedProps['apiKey'],
15
15
  EmbedProps['data'],
@@ -17,10 +17,13 @@ type TaskDeps = [
17
17
  EmbedProps['templateId']
18
18
  ];
19
19
 
20
+ type FilterTaskDeps = [EmbedProps['data'], EmbedProps['questionIndexes']];
21
+
20
22
  class FetcherController {
21
23
  host: EmbedHost;
22
24
 
23
25
  private _fetching = false;
26
+ private _vouch: EmbedProps['data'];
24
27
 
25
28
  set fetching(value) {
26
29
  if (this._fetching !== value) {
@@ -45,13 +48,13 @@ class FetcherController {
45
48
  });
46
49
 
47
50
  const vouch = await res.json();
48
- this.host.dispatchEvent(new CustomEvent('vouch:loaded', { detail: vouchId }));
51
+ this.host.dispatchEvent(new CustomEvent('vouch:loaded', { detail: vouch?.id }));
49
52
 
50
53
  // HACK: we're currently using API Gateway caching on the embed API without any invalidation logic,
51
54
  // so to ensure that the cache stays up to date, whenever we detect a cache hit we trigger another
52
55
  // API call with the `Cache-Control` header which will re-fill the cache
53
56
  const resCacheCheck = res?.headers?.get('X-Cache-Check');
54
- if (resCacheCheck && resCacheCheck !== cacheCheck) {
57
+ if (resCacheCheck !== cacheCheck) {
55
58
  fetch(`${embedApiUrl}/vouches/${vouchId}`, {
56
59
  method: 'GET',
57
60
  headers: [
@@ -81,7 +84,7 @@ class FetcherController {
81
84
  // so to ensure that the cache stays up to date, whenever we detect a cache hit we trigger another
82
85
  // API call with the `Cache-Control` header which will re-fill the cache
83
86
  const resCacheCheck = res?.headers?.get('X-Cache-Check');
84
- if (resCacheCheck && resCacheCheck !== cacheCheck) {
87
+ if (resCacheCheck !== cacheCheck) {
85
88
  fetch(`${embedApiUrl}/templates/${templateId}`, {
86
89
  method: 'GET',
87
90
  headers: [
@@ -96,9 +99,9 @@ class FetcherController {
96
99
 
97
100
  constructor(host: EmbedHost) {
98
101
  this.host = host;
99
- new Task<TaskDeps, void>(
102
+ new Task<FetchTaskDeps, void>(
100
103
  this.host,
101
- async ([env, apiKey, data, vouchId, templateId]: TaskDeps) => {
104
+ async ([env, apiKey, data, vouchId, templateId]: FetchTaskDeps) => {
102
105
  try {
103
106
  host.vouch = undefined;
104
107
  host.template = undefined;
@@ -109,7 +112,7 @@ class FetcherController {
109
112
  this.fetching = true;
110
113
  template = await this.getTemplate(env, apiKey, templateId);
111
114
  }
112
- host.vouch = data;
115
+ this._vouch = data;
113
116
  host.template = template ?? data?.settings?.template?.instance;
114
117
  } else if (vouchId) {
115
118
  this.fetching = true;
@@ -118,7 +121,7 @@ class FetcherController {
118
121
  this.getVouch(env, apiKey, vouchId),
119
122
  templateId ? this.getTemplate(env, apiKey, templateId) : null
120
123
  ]);
121
- host.vouch = vouch;
124
+ this._vouch = vouch;
122
125
  host.template = template ?? vouch?.settings?.template?.instance;
123
126
  }
124
127
  } finally {
@@ -127,6 +130,24 @@ class FetcherController {
127
130
  },
128
131
  () => [host.env, host.apiKey, host.data, host.vouchId, host.templateId]
129
132
  );
133
+
134
+ // This second task is to be able to filter the vouch without fetching it again if only the questionIndexes changed
135
+ new Task<FilterTaskDeps, void>(
136
+ this.host,
137
+ ([vouch, questionIndexes]: FilterTaskDeps) => {
138
+ host.vouch = vouch
139
+ ? {
140
+ ...vouch,
141
+ questions: {
142
+ items: vouch?.questions.items.filter(
143
+ (_, index) => !questionIndexes?.length || questionIndexes?.includes(index + 1)
144
+ )
145
+ }
146
+ }
147
+ : undefined;
148
+ },
149
+ () => [this._vouch, host.questionIndexes]
150
+ );
130
151
  }
131
152
  }
132
153
 
@@ -1,19 +1,19 @@
1
- /* eslint-disable max-lines */
2
1
  import { v4 as uuidv4 } from 'uuid';
3
2
 
4
3
  import type { Embed } from '..';
5
4
  import type { VideoEventDetail } from '@vouchfor/media-player';
6
5
  import type { ReactiveController, ReactiveControllerHost } from 'lit';
7
6
 
7
+ import packageJson from '../../../../package.json';
8
8
  import { getEnvUrls } from '~/utils/env';
9
9
 
10
- const STREAMED_THROTTLE = 10000;
10
+ const MINIMUM_SEND_THRESHOLD = 1;
11
11
 
12
12
  type EmbedHost = ReactiveControllerHost & Embed;
13
13
 
14
14
  type TrackingEvent = 'VOUCH_LOADED' | 'VOUCH_RESPONSE_VIEWED' | 'VIDEO_PLAYED' | 'VIDEO_STREAMED';
15
15
  type TrackingPayload = {
16
- vouchId: string;
16
+ vouchId?: string;
17
17
  answerId?: string;
18
18
  streamStart?: number;
19
19
  streamEnd?: number;
@@ -37,22 +37,23 @@ class TrackingController implements ReactiveController {
37
37
  private _hasPlayed = false;
38
38
  private _hasLoaded: BooleanMap = {};
39
39
  private _answersViewed: BooleanMap = {};
40
- private _streamedTime: TimeMap = {};
40
+ private _streamStartTime: TimeMap = {};
41
41
  private _streamLatestTime: TimeMap = {};
42
- private _streamedPrevTimestamp: TimeMap = {};
42
+ private _currentlyPlayingVideo: VideoEventDetail | null = null;
43
43
 
44
44
  constructor(host: EmbedHost) {
45
45
  this.host = host;
46
46
  host.addController(this);
47
47
  }
48
48
 
49
- private _findVouchId() {
49
+ private _findVouchId(payload?: TrackingPayload) {
50
+ if (payload && 'vouchId' in payload) {
51
+ return payload.vouchId;
52
+ }
50
53
  if (this.host.vouch) {
51
- if ('uuid' in this.host.vouch) {
52
- return this.host.vouch.uuid;
53
- }
54
54
  return this.host.vouch.id;
55
55
  }
56
+ return null;
56
57
  }
57
58
 
58
59
  private _createVisitor = (visitorId: string) => {
@@ -130,30 +131,56 @@ class TrackingController implements ReactiveController {
130
131
  };
131
132
  };
132
133
 
133
- private _sendTrackingEvent = (event: TrackingEvent, payload: TrackingPayload) => {
134
- const { publicApiUrl } = getEnvUrls(this.host.env);
135
- const { client, tab, request, visitor } = this._getUids();
134
+ private _sendTrackingEvent = (event: TrackingEvent, payload?: TrackingPayload) => {
135
+ const vouchId = this._findVouchId(payload);
136
136
 
137
- if (this.host.disableTracking) {
137
+ if (!vouchId || this.host.disableTracking) {
138
138
  return;
139
139
  }
140
140
 
141
+ const { publicApiUrl } = getEnvUrls(this.host.env);
142
+ const { client, tab, request, visitor } = this._getUids();
143
+
141
144
  navigator.sendBeacon(
142
- `${publicApiUrl}/api/events`,
145
+ `${publicApiUrl}/api/v2/events`,
143
146
  JSON.stringify({
144
147
  event,
145
- payload,
148
+ payload: {
149
+ ...payload,
150
+ vouchId
151
+ },
146
152
  context: {
147
153
  'x-uid-client': client,
148
154
  'x-uid-tab': tab,
149
155
  'x-uid-request': request,
150
156
  'x-uid-visitor': visitor,
151
- 'x-reporting-metadata': this._getReportingMetadata()
157
+ 'x-reporting-metadata': this._getReportingMetadata(),
158
+ 'x-embeds-version': packageJson.version
152
159
  }
153
160
  })
154
161
  );
155
162
  };
156
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
+
157
184
  private _handleVouchLoaded = ({ detail: vouchId }: CustomEvent<string>) => {
158
185
  if (!vouchId) {
159
186
  return;
@@ -161,24 +188,15 @@ class TrackingController implements ReactiveController {
161
188
 
162
189
  // Only send loaded event once per session
163
190
  if (!this._hasLoaded[vouchId]) {
164
- this._sendTrackingEvent('VOUCH_LOADED', {
165
- vouchId
166
- });
191
+ this._sendTrackingEvent('VOUCH_LOADED', { vouchId });
167
192
  this._hasLoaded[vouchId] = true;
168
193
  }
169
194
  };
170
195
 
171
196
  private _handlePlay = () => {
172
- const vouchId = this._findVouchId();
173
-
174
- if (!vouchId) {
175
- return;
176
- }
177
-
178
197
  // Only send the video played event once per session
179
198
  if (!this._hasPlayed) {
180
199
  this._sendTrackingEvent('VIDEO_PLAYED', {
181
- vouchId,
182
200
  streamStart: this.host.currentTime
183
201
  });
184
202
  this._hasPlayed = true;
@@ -186,118 +204,86 @@ class TrackingController implements ReactiveController {
186
204
  };
187
205
 
188
206
  private _handleVideoPlay = ({ detail: { id, key, node } }: CustomEvent<VideoEventDetail>) => {
189
- const vouchId = this._findVouchId();
190
-
191
- if (!vouchId) {
192
- return;
193
- }
194
-
195
207
  // Only increment play count once per session
196
208
  if (!this._answersViewed[key]) {
197
209
  this._sendTrackingEvent('VOUCH_RESPONSE_VIEWED', {
198
- vouchId,
199
210
  answerId: id
200
211
  });
201
212
  this._answersViewed[key] = true;
202
213
  }
203
214
 
204
- this._streamedTime[key] = node.currentTime;
205
- this._streamLatestTime[key] = node.currentTime;
206
- this._streamedPrevTimestamp[key] = Date.now();
207
- };
208
-
209
- private _handleVideoSeeking = ({ detail: { id, key } }: CustomEvent<VideoEventDetail>) => {
210
- const vouchId = this._findVouchId();
211
-
212
- if (!vouchId) {
213
- return;
215
+ if (!this._streamStartTime[key]) {
216
+ this._streamStartTime[key] = node.currentTime;
217
+ this._streamLatestTime[key] = node.currentTime;
214
218
  }
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
219
  };
229
220
 
230
221
  private _handleVideoTimeUpdate = ({ detail: { id, key, node } }: CustomEvent<VideoEventDetail>) => {
231
- const vouchId = this._findVouchId();
232
-
233
- if (!vouchId) {
234
- return;
235
- }
236
-
237
- const currentTimestamp = Date.now();
238
222
  if (
239
- node.currentTime &&
240
- !node.paused &&
223
+ // We only want to count any time that the video is actually playing
241
224
  !this.host.paused &&
242
- // Only fire the video seeked event when this video is the active one
243
- id === this.host.scene?.video?.id &&
244
- // Throttle the frequency that we send streamed events while playing
245
- currentTimestamp - this._streamedPrevTimestamp[key] > STREAMED_THROTTLE
225
+ // Only update the latest time if this event fires for the currently active video
226
+ id === this.host.scene?.video?.id
246
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) {
247
235
  this._sendTrackingEvent('VIDEO_STREAMED', {
248
- vouchId,
249
236
  answerId: id,
250
- streamStart: this._streamedTime[key],
251
- streamEnd: node.currentTime
237
+ streamStart: this._streamStartTime[key],
238
+ streamEnd: this._streamLatestTime[key]
252
239
  });
253
- this._streamedTime[key] = node.currentTime;
254
- this._streamedPrevTimestamp[key] = currentTimestamp;
255
240
  }
256
-
257
- this._streamLatestTime[key] = node.currentTime;
241
+ delete this._streamStartTime[key];
242
+ delete this._streamLatestTime[key];
258
243
  };
259
244
 
260
- private _handleVideoPause = ({ detail: { id, key, node } }: CustomEvent<VideoEventDetail>) => {
261
- const vouchId = this._findVouchId();
262
-
263
- if (!vouchId) {
264
- return;
265
- }
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
+ };
266
251
 
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
- });
252
+ private _handleVisibilityChange = () => {
253
+ if (document.visibilityState === 'hidden') {
254
+ this._pageUnloading();
278
255
  }
256
+ };
279
257
 
280
- delete this._streamedTime[key];
281
- delete this._streamLatestTime[key];
282
- delete this._streamedPrevTimestamp[key];
258
+ private _handlePageHide = () => {
259
+ this._pageUnloading();
283
260
  };
284
261
 
285
262
  hostConnected() {
286
263
  requestAnimationFrame(() => {
264
+ if ('onvisibilitychange' in document) {
265
+ document.addEventListener('visibilitychange', this._handleVisibilityChange);
266
+ } else {
267
+ window.addEventListener('pagehide', this._handlePageHide);
268
+ }
287
269
  this.host.addEventListener('vouch:loaded', this._handleVouchLoaded);
288
270
  this.host.mediaPlayer?.addEventListener('play', this._handlePlay);
289
271
  this.host.mediaPlayer?.addEventListener('video:play', this._handleVideoPlay);
290
- this.host.mediaPlayer?.addEventListener('video:seeking', this._handleVideoSeeking);
291
272
  this.host.mediaPlayer?.addEventListener('video:pause', this._handleVideoPause);
292
273
  this.host.mediaPlayer?.addEventListener('video:timeupdate', this._handleVideoTimeUpdate);
293
274
  });
294
275
  }
295
276
 
296
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
+ }
297
284
  this.host.removeEventListener('vouch:loaded', this._handleVouchLoaded);
298
285
  this.host.mediaPlayer?.removeEventListener('play', this._handlePlay);
299
286
  this.host.mediaPlayer?.removeEventListener('video:play', this._handleVideoPlay);
300
- this.host.mediaPlayer?.removeEventListener('video:seeking', this._handleVideoSeeking);
301
287
  this.host.mediaPlayer?.removeEventListener('video:pause', this._handleVideoPause);
302
288
  this.host.mediaPlayer?.removeEventListener('video:timeupdate', this._handleVideoTimeUpdate);
303
289
  }
@@ -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';
@@ -21,15 +21,18 @@ type EmbedProps = Pick<MediaPlayerProps, 'data' | 'aspectRatio' | 'preload' | 'a
21
21
  trackingSource?: string;
22
22
  vouchId?: string;
23
23
  templateId?: string;
24
+ // Index of the questions to include starting from 1
25
+ questionIndexes?: number[];
24
26
  };
25
27
 
26
28
  @customElement('vouch-embed')
27
29
  class Embed extends LitElement {
28
30
  private _mediaPlayerRef: Ref<MediaPlayer> = createRef();
29
31
 
30
- @property({ type: Object, attribute: 'data' }) data: EmbedProps['data'];
32
+ @property({ type: Object }) data: EmbedProps['data'];
31
33
  @property({ type: String }) vouchId: EmbedProps['vouchId'];
32
34
  @property({ type: String }) templateId: EmbedProps['templateId'];
35
+ @property({ type: Array }) questionIndexes: EmbedProps['questionIndexes'];
33
36
 
34
37
  @property({ type: String }) env: EmbedProps['env'] = 'prod';
35
38
  @property({ type: String }) apiKey: EmbedProps['apiKey'] = '';
@@ -149,6 +152,10 @@ class Embed extends LitElement {
149
152
  return this._mediaPlayerRef.value?.scenes ?? [];
150
153
  }
151
154
 
155
+ get sceneConfig(): Scenes | null {
156
+ return this._mediaPlayerRef.value?.sceneConfig ?? null;
157
+ }
158
+
152
159
  get videoState() {
153
160
  return this._mediaPlayerRef.value?.videoState;
154
161
  }
File without changes
File without changes
File without changes