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

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.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