@vouchfor/embeds 0.0.0-experiment.8a05fac → 0.0.0-experiment.8aa25a2

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.8a05fac",
3
+ "version": "0.0.0-experiment.8aa25a2",
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.8a05fac",
39
+ "@vouchfor/media-player": "0.0.0-experiment.8aa25a2",
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,20 +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
- // In seconds due to checking against node.currentTime
11
- const STREAMED_THROTTLE = 10;
10
+ const MINIMUM_SEND_THRESHOLD = 1;
12
11
 
13
12
  type EmbedHost = ReactiveControllerHost & Embed;
14
13
 
15
14
  type TrackingEvent = 'VOUCH_LOADED' | 'VOUCH_RESPONSE_VIEWED' | 'VIDEO_PLAYED' | 'VIDEO_STREAMED';
16
15
  type TrackingPayload = {
17
- vouchId: string;
16
+ vouchId?: string;
18
17
  answerId?: string;
19
18
  streamStart?: number;
20
19
  streamEnd?: number;
@@ -38,21 +37,23 @@ class TrackingController implements ReactiveController {
38
37
  private _hasPlayed = false;
39
38
  private _hasLoaded: BooleanMap = {};
40
39
  private _answersViewed: BooleanMap = {};
41
- private _streamedTime: TimeMap = {};
40
+ private _streamStartTime: TimeMap = {};
42
41
  private _streamLatestTime: 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,81 +204,68 @@ 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;
215
+ if (!this._streamStartTime[key]) {
216
+ this._streamStartTime[key] = node.currentTime;
217
+ this._streamLatestTime[key] = node.currentTime;
218
+ }
206
219
  };
207
220
 
208
221
  private _handleVideoTimeUpdate = ({ detail: { id, key, node } }: CustomEvent<VideoEventDetail>) => {
209
- const vouchId = this._findVouchId();
210
-
211
- if (!vouchId) {
212
- return;
213
- }
214
-
215
222
  if (
216
- node.currentTime &&
217
- !node.paused &&
223
+ // We only want to count any time that the video is actually playing
218
224
  !this.host.paused &&
219
- // Only fire the video seeked event when this video is the active one
220
- id === this.host.scene?.video?.id &&
221
- // Throttle the frequency that we send streamed events while playing
222
- node.currentTime - this._streamedTime[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
223
227
  ) {
224
- this._sendTrackingEvent('VIDEO_STREAMED', {
225
- vouchId,
226
- answerId: id,
227
- streamStart: this._streamedTime[key],
228
- streamEnd: node.currentTime
229
- });
230
- this._streamedTime[key] = node.currentTime;
231
- }
232
-
233
- if (!this.host.paused) {
228
+ this._currentlyPlayingVideo = { id, key, node };
234
229
  this._streamLatestTime[key] = node.currentTime;
235
230
  }
236
231
  };
237
232
 
238
233
  private _handleVideoPause = ({ detail: { id, key } }: CustomEvent<VideoEventDetail>) => {
239
- const vouchId = this._findVouchId();
240
-
241
- if (!vouchId) {
242
- return;
243
- }
244
-
245
- // Don't send a tracking event if the video pauses when seeking backwards
246
- if (this._streamLatestTime[key] > this._streamedTime[key] + 0.5) {
247
- // Send a video streamed event any time the video pauses then reset the streamed state
248
- // We do this to capture the last bit of time that the video was played between the previous
249
- // stream event and the video being paused manually or stopping because it ended
234
+ if (this._streamLatestTime[key] > this._streamStartTime[key] + MINIMUM_SEND_THRESHOLD) {
250
235
  this._sendTrackingEvent('VIDEO_STREAMED', {
251
- vouchId,
252
236
  answerId: id,
253
- streamStart: this._streamedTime[key],
237
+ streamStart: this._streamStartTime[key],
254
238
  streamEnd: this._streamLatestTime[key]
255
239
  });
256
240
  }
257
-
258
- delete this._streamedTime[key];
241
+ delete this._streamStartTime[key];
259
242
  delete this._streamLatestTime[key];
260
243
  };
261
244
 
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();
255
+ }
256
+ };
257
+
258
+ private _handlePageHide = () => {
259
+ this._pageUnloading();
260
+ };
261
+
262
262
  hostConnected() {
263
263
  requestAnimationFrame(() => {
264
+ if ('onvisibilitychange' in document) {
265
+ document.addEventListener('visibilitychange', this._handleVisibilityChange);
266
+ } else {
267
+ window.addEventListener('pagehide', this._handlePageHide);
268
+ }
264
269
  this.host.addEventListener('vouch:loaded', this._handleVouchLoaded);
265
270
  this.host.mediaPlayer?.addEventListener('play', this._handlePlay);
266
271
  this.host.mediaPlayer?.addEventListener('video:play', this._handleVideoPlay);
@@ -270,6 +275,12 @@ class TrackingController implements ReactiveController {
270
275
  }
271
276
 
272
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
+ }
273
284
  this.host.removeEventListener('vouch:loaded', this._handleVouchLoaded);
274
285
  this.host.mediaPlayer?.removeEventListener('play', this._handlePlay);
275
286
  this.host.mediaPlayer?.removeEventListener('video:play', this._handleVideoPlay);
File without changes
File without changes
File without changes