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

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.b7c103f",
3
+ "version": "0.0.0-experiment.bb5f326",
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.b7c103f",
39
+ "@vouchfor/media-player": "0.0.0-experiment.bb5f326",
40
40
  "uuid": "^9.0.1"
41
41
  },
42
42
  "peerDependencies": {
@@ -45,13 +45,13 @@ class FetcherController {
45
45
  });
46
46
 
47
47
  const vouch = await res.json();
48
- this.host.dispatchEvent(new CustomEvent('vouch:loaded', { detail: vouchId }));
48
+ this.host.dispatchEvent(new CustomEvent('vouch:loaded', { detail: vouch?.id }));
49
49
 
50
50
  // HACK: we're currently using API Gateway caching on the embed API without any invalidation logic,
51
51
  // so to ensure that the cache stays up to date, whenever we detect a cache hit we trigger another
52
52
  // API call with the `Cache-Control` header which will re-fill the cache
53
53
  const resCacheCheck = res?.headers?.get('X-Cache-Check');
54
- if (resCacheCheck && resCacheCheck !== cacheCheck) {
54
+ if (resCacheCheck !== cacheCheck) {
55
55
  fetch(`${embedApiUrl}/vouches/${vouchId}`, {
56
56
  method: 'GET',
57
57
  headers: [
@@ -81,7 +81,7 @@ class FetcherController {
81
81
  // so to ensure that the cache stays up to date, whenever we detect a cache hit we trigger another
82
82
  // API call with the `Cache-Control` header which will re-fill the cache
83
83
  const resCacheCheck = res?.headers?.get('X-Cache-Check');
84
- if (resCacheCheck && resCacheCheck !== cacheCheck) {
84
+ if (resCacheCheck !== cacheCheck) {
85
85
  fetch(`${embedApiUrl}/templates/${templateId}`, {
86
86
  method: 'GET',
87
87
  headers: [
@@ -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';
@@ -149,6 +149,10 @@ class Embed extends LitElement {
149
149
  return this._mediaPlayerRef.value?.scenes ?? [];
150
150
  }
151
151
 
152
+ get sceneConfig(): Scenes | null {
153
+ return this._mediaPlayerRef.value?.sceneConfig ?? null;
154
+ }
155
+
152
156
  get videoState() {
153
157
  return this._mediaPlayerRef.value?.videoState;
154
158
  }
File without changes
File without changes
File without changes