@vouchfor/embeds 0.0.0-experiment.de7febc → 0.0.0-experiment.e14607a

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.de7febc",
3
+ "version": "0.0.0-experiment.e14607a",
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.de7febc",
39
+ "@vouchfor/media-player": "0.0.0-experiment.e14607a",
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: [
@@ -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 = 10000;
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() {
49
+ private _findVouchId(payload?: TrackingPayload) {
50
+ if (payload && 'vouchId' in payload) {
51
+ return payload.vouchId;
52
+ }
48
53
  if (this.host.vouch) {
49
- if ('uuid' in this.host.vouch) {
50
- return this.host.vouch.uuid;
51
- }
52
54
  return this.host.vouch.id;
53
55
  }
56
+ return null;
54
57
  }
55
58
 
56
59
  private _createVisitor = (visitorId: string) => {
@@ -128,25 +131,53 @@ 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
- if (this.host.enableTracking) {
136
- navigator.sendBeacon(
137
- `${publicApiUrl}/api/events`,
138
- JSON.stringify({
139
- event,
140
- payload,
141
- context: {
142
- 'x-uid-client': client,
143
- 'x-uid-tab': tab,
144
- 'x-uid-request': request,
145
- 'x-uid-visitor': visitor,
146
- 'x-reporting-metadata': this._getReportingMetadata()
147
- }
148
- })
149
- );
144
+ navigator.sendBeacon(
145
+ `${publicApiUrl}/api/v2/events`,
146
+ JSON.stringify({
147
+ event,
148
+ payload: {
149
+ ...payload,
150
+ vouchId
151
+ },
152
+ context: {
153
+ 'x-uid-client': client,
154
+ 'x-uid-tab': tab,
155
+ 'x-uid-request': request,
156
+ 'x-uid-visitor': visitor,
157
+ 'x-reporting-metadata': this._getReportingMetadata(),
158
+ 'x-embeds-version': packageJson.version
159
+ }
160
+ })
161
+ );
162
+ };
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];
150
181
  }
151
182
  };
152
183
 
@@ -157,100 +188,84 @@ class TrackingController implements ReactiveController {
157
188
 
158
189
  // Only send loaded event once per session
159
190
  if (!this._hasLoaded[vouchId]) {
160
- this._sendTrackingEvent('VOUCH_LOADED', {
161
- vouchId
162
- });
191
+ this._sendTrackingEvent('VOUCH_LOADED', { vouchId });
163
192
  this._hasLoaded[vouchId] = true;
164
193
  }
165
194
  };
166
195
 
167
196
  private _handlePlay = () => {
168
- const vouchId = this._findVouchId();
169
-
170
- if (!vouchId) {
171
- return;
172
- }
173
-
174
197
  // Only send the video played event once per session
175
198
  if (!this._hasPlayed) {
176
199
  this._sendTrackingEvent('VIDEO_PLAYED', {
177
- vouchId,
178
200
  streamStart: this.host.currentTime
179
201
  });
180
202
  this._hasPlayed = true;
181
203
  }
182
204
  };
183
205
 
184
- private _handleVideoPlay = ({ detail: { id, node } }: CustomEvent<VideoEventDetail>) => {
185
- const vouchId = this._findVouchId();
186
-
187
- if (!vouchId) {
188
- return;
189
- }
206
+ private _handleVideoPlay = ({ detail: { id, key, node } }: CustomEvent<VideoEventDetail>) => {
190
207
  // Only increment play count once per session
191
- if (!this._answersViewed[id]) {
208
+ if (!this._answersViewed[key]) {
192
209
  this._sendTrackingEvent('VOUCH_RESPONSE_VIEWED', {
193
- vouchId,
194
210
  answerId: id
195
211
  });
196
- this._answersViewed[id] = true;
212
+ this._answersViewed[key] = true;
197
213
  }
198
- this._streamedTime[id] = node.currentTime;
199
- this._streamedPrevTimestamp[id] = Date.now();
200
- };
201
-
202
- private _handleVideoTimeUpdate = ({ detail: { id, node } }: CustomEvent<VideoEventDetail>) => {
203
- const vouchId = this._findVouchId();
204
214
 
205
- if (!vouchId) {
206
- return;
215
+ if (!this._streamStartTime[key]) {
216
+ this._streamStartTime[key] = node.currentTime;
217
+ this._streamLatestTime[key] = node.currentTime;
207
218
  }
208
- const currentTimestamp = Date.now();
219
+ };
220
+
221
+ private _handleVideoTimeUpdate = ({ detail: { id, key, node } }: CustomEvent<VideoEventDetail>) => {
209
222
  if (
210
- node.currentTime &&
211
- !node.paused &&
223
+ // We only want to count any time that the video is actually playing
212
224
  !this.host.paused &&
213
- // Only fire the video seeked event when this video is the active one
214
- id === this.host.scene?.video?.id &&
215
- // Throttle the frequency that we send streamed events while playing
216
- 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
217
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) {
218
235
  this._sendTrackingEvent('VIDEO_STREAMED', {
219
- vouchId,
220
236
  answerId: id,
221
- streamStart: this._streamedTime[id],
222
- streamEnd: node.currentTime
237
+ streamStart: this._streamStartTime[key],
238
+ streamEnd: this._streamLatestTime[key]
223
239
  });
224
- this._streamedTime[id] = node.currentTime;
225
- this._streamedPrevTimestamp[id] = currentTimestamp;
226
240
  }
241
+ delete this._streamStartTime[key];
242
+ delete this._streamLatestTime[key];
227
243
  };
228
244
 
229
- private _handleVideoPause = ({ detail: { id, node } }: CustomEvent<VideoEventDetail>) => {
230
- const vouchId = this._findVouchId();
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
+ };
231
251
 
232
- if (!vouchId) {
233
- return;
252
+ private _handleVisibilityChange = () => {
253
+ if (document.visibilityState === 'hidden') {
254
+ this._pageUnloading();
234
255
  }
256
+ };
235
257
 
236
- // Don't send a tracking event if the video pauses when seeking backwards
237
- if (node.currentTime > this._streamedTime[id]) {
238
- // Send a video streamed event any time the video pauses then reset the streamed state
239
- // We do this to capture the last bit of time that the video was played between the previous
240
- // stream event and the video being paused manually or stopping because it ended
241
- this._sendTrackingEvent('VIDEO_STREAMED', {
242
- vouchId,
243
- answerId: id,
244
- streamStart: this._streamedTime[id],
245
- streamEnd: node.currentTime
246
- });
247
- }
248
- delete this._streamedTime[id];
249
- delete this._streamedPrevTimestamp[id];
258
+ private _handlePageHide = () => {
259
+ this._pageUnloading();
250
260
  };
251
261
 
252
262
  hostConnected() {
253
263
  requestAnimationFrame(() => {
264
+ if ('onvisibilitychange' in document) {
265
+ document.addEventListener('visibilitychange', this._handleVisibilityChange);
266
+ } else {
267
+ window.addEventListener('pagehide', this._handlePageHide);
268
+ }
254
269
  this.host.addEventListener('vouch:loaded', this._handleVouchLoaded);
255
270
  this.host.mediaPlayer?.addEventListener('play', this._handlePlay);
256
271
  this.host.mediaPlayer?.addEventListener('video:play', this._handleVideoPlay);
@@ -260,6 +275,12 @@ class TrackingController implements ReactiveController {
260
275
  }
261
276
 
262
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
+ }
263
284
  this.host.removeEventListener('vouch:loaded', this._handleVouchLoaded);
264
285
  this.host.mediaPlayer?.removeEventListener('play', this._handlePlay);
265
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,7 +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
- enableTracking?: boolean;
20
+ disableTracking?: boolean;
21
21
  trackingSource?: string;
22
22
  vouchId?: string;
23
23
  templateId?: string;
@@ -33,7 +33,7 @@ class Embed extends LitElement {
33
33
 
34
34
  @property({ type: String }) env: EmbedProps['env'] = 'prod';
35
35
  @property({ type: String }) apiKey: EmbedProps['apiKey'] = '';
36
- @property({ type: Boolean }) enableTracking: EmbedProps['enableTracking'] = true;
36
+ @property({ type: Boolean }) disableTracking: EmbedProps['disableTracking'] = false;
37
37
  @property({ type: String }) trackingSource: EmbedProps['trackingSource'] = 'embed';
38
38
 
39
39
  @property({ type: Array }) controls: EmbedProps['controls'];
@@ -59,6 +59,7 @@ class Embed extends LitElement {
59
59
  'waiting',
60
60
 
61
61
  'video:loadeddata',
62
+ 'video:seeking',
62
63
  'video:seeked',
63
64
  'video:play',
64
65
  'video:playing',
@@ -148,6 +149,10 @@ class Embed extends LitElement {
148
149
  return this._mediaPlayerRef.value?.scenes ?? [];
149
150
  }
150
151
 
152
+ get sceneConfig(): Scenes | null {
153
+ return this._mediaPlayerRef.value?.sceneConfig ?? null;
154
+ }
155
+
151
156
  get videoState() {
152
157
  return this._mediaPlayerRef.value?.videoState;
153
158
  }
File without changes
File without changes
File without changes