@vouchfor/embeds 0.0.0-experiment.5e5848e → 0.0.0-experiment.607fdcd
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/dist/es/components/Embed/controllers/tracking.d.ts +3 -2
- package/dist/es/components/Embed/index.d.ts +2 -0
- package/dist/es/embeds.js +459 -447
- package/dist/es/embeds.js.map +1 -1
- package/dist/iife/embeds.iife.js +218 -189
- package/dist/iife/embeds.iife.js.map +1 -1
- package/package.json +2 -2
- package/src/components/Embed/controllers/fetcher.ts +39 -19
- package/src/components/Embed/controllers/tracking.ts +66 -58
- package/src/components/Embed/index.ts +3 -0
package/package.json
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
{
|
2
2
|
"name": "@vouchfor/embeds",
|
3
|
-
"version": "0.0.0-experiment.
|
3
|
+
"version": "0.0.0-experiment.607fdcd",
|
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.
|
39
|
+
"@vouchfor/media-player": "0.0.0-experiment.607fdcd",
|
40
40
|
"uuid": "^9.0.1"
|
41
41
|
},
|
42
42
|
"peerDependencies": {
|
@@ -1,4 +1,5 @@
|
|
1
1
|
import { Task } from '@lit/task';
|
2
|
+
import { v4 as uuidv4 } from 'uuid';
|
2
3
|
|
3
4
|
import type { Embed, EmbedProps } from '..';
|
4
5
|
import type { ReactiveControllerHost } from 'lit';
|
@@ -34,42 +35,61 @@ class FetcherController {
|
|
34
35
|
private getVouch = async (env: Environment, apiKey: string, vouchId: string) => {
|
35
36
|
const { embedApiUrl } = getEnvUrls(env);
|
36
37
|
|
37
|
-
const
|
38
|
-
|
39
|
-
headers: [['X-Api-Key', apiKey]]
|
40
|
-
}).then((response) => {
|
41
|
-
this.host.dispatchEvent(new CustomEvent('vouch:loaded', { detail: vouchId }));
|
42
|
-
return response.json();
|
43
|
-
});
|
44
|
-
|
45
|
-
// HACK: trigger another fetch after we received the data to update the cache in the background
|
46
|
-
fetch(`${embedApiUrl}/vouches/${vouchId}`, {
|
38
|
+
const cacheCheck = uuidv4();
|
39
|
+
const res = await fetch(`${embedApiUrl}/vouches/${vouchId}`, {
|
47
40
|
method: 'GET',
|
48
41
|
headers: [
|
49
42
|
['X-Api-Key', apiKey],
|
50
|
-
['Cache-
|
43
|
+
['X-Cache-Check', cacheCheck]
|
51
44
|
]
|
52
45
|
});
|
53
46
|
|
47
|
+
const vouch = await res.json();
|
48
|
+
this.host.dispatchEvent(new CustomEvent('vouch:loaded', { detail: vouchId }));
|
49
|
+
|
50
|
+
// HACK: we're currently using API Gateway caching on the embed API without any invalidation logic,
|
51
|
+
// so to ensure that the cache stays up to date, whenever we detect a cache hit we trigger another
|
52
|
+
// API call with the `Cache-Control` header which will re-fill the cache
|
53
|
+
const resCacheCheck = res?.headers?.get('X-Cache-Check');
|
54
|
+
if (resCacheCheck !== cacheCheck) {
|
55
|
+
fetch(`${embedApiUrl}/vouches/${vouchId}`, {
|
56
|
+
method: 'GET',
|
57
|
+
headers: [
|
58
|
+
['X-Api-Key', apiKey],
|
59
|
+
['Cache-Control', 'max-age=0']
|
60
|
+
]
|
61
|
+
});
|
62
|
+
}
|
63
|
+
|
54
64
|
return vouch;
|
55
65
|
};
|
56
66
|
|
57
67
|
private getTemplate = async (env: Environment, apiKey: string, templateId: string) => {
|
58
68
|
const { embedApiUrl } = getEnvUrls(env);
|
59
69
|
|
60
|
-
const
|
61
|
-
|
62
|
-
headers: [['X-Api-Key', apiKey]]
|
63
|
-
}).then((response) => response.json());
|
64
|
-
|
65
|
-
// HACK: trigger another fetch after we received the data to update the cache in the background
|
66
|
-
fetch(`${embedApiUrl}/templates/${templateId}`, {
|
70
|
+
const cacheCheck = uuidv4();
|
71
|
+
const res = await fetch(`${embedApiUrl}/templates/${templateId}`, {
|
67
72
|
method: 'GET',
|
68
73
|
headers: [
|
69
74
|
['X-Api-Key', apiKey],
|
70
|
-
['Cache-
|
75
|
+
['X-Cache-Check', cacheCheck]
|
71
76
|
]
|
72
77
|
});
|
78
|
+
const template = await res.json();
|
79
|
+
|
80
|
+
// HACK: we're currently using API Gateway caching on the embed API without any invalidation logic,
|
81
|
+
// so to ensure that the cache stays up to date, whenever we detect a cache hit we trigger another
|
82
|
+
// API call with the `Cache-Control` header which will re-fill the cache
|
83
|
+
const resCacheCheck = res?.headers?.get('X-Cache-Check');
|
84
|
+
if (resCacheCheck !== cacheCheck) {
|
85
|
+
fetch(`${embedApiUrl}/templates/${templateId}`, {
|
86
|
+
method: 'GET',
|
87
|
+
headers: [
|
88
|
+
['X-Api-Key', apiKey],
|
89
|
+
['Cache-Control', 'max-age=0']
|
90
|
+
]
|
91
|
+
});
|
92
|
+
}
|
73
93
|
|
74
94
|
return template;
|
75
95
|
};
|
@@ -6,13 +6,13 @@ import type { ReactiveController, ReactiveControllerHost } from 'lit';
|
|
6
6
|
|
7
7
|
import { getEnvUrls } from '~/utils/env';
|
8
8
|
|
9
|
-
|
9
|
+
// In seconds due to checking against node.currentTime
|
10
|
+
const STREAMED_THROTTLE = 10;
|
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
16
|
answerId?: string;
|
17
17
|
streamStart?: number;
|
18
18
|
streamEnd?: number;
|
@@ -36,8 +36,9 @@ class TrackingController implements ReactiveController {
|
|
36
36
|
private _hasPlayed = false;
|
37
37
|
private _hasLoaded: BooleanMap = {};
|
38
38
|
private _answersViewed: BooleanMap = {};
|
39
|
-
private
|
40
|
-
private
|
39
|
+
private _streamStartTime: TimeMap = {};
|
40
|
+
private _streamLatestTime: TimeMap = {};
|
41
|
+
private _currentlyPlayingVideo: VideoEventDetail | null = null;
|
41
42
|
|
42
43
|
constructor(host: EmbedHost) {
|
43
44
|
this.host = host;
|
@@ -45,12 +46,10 @@ class TrackingController implements ReactiveController {
|
|
45
46
|
}
|
46
47
|
|
47
48
|
private _findVouchId() {
|
48
|
-
if (this.host.
|
49
|
-
|
50
|
-
return this.host.data.uuid;
|
51
|
-
}
|
52
|
-
return this.host.data.id;
|
49
|
+
if (this.host.vouch) {
|
50
|
+
return this.host.vouch.id;
|
53
51
|
}
|
52
|
+
return null;
|
54
53
|
}
|
55
54
|
|
56
55
|
private _createVisitor = (visitorId: string) => {
|
@@ -128,16 +127,24 @@ class TrackingController implements ReactiveController {
|
|
128
127
|
};
|
129
128
|
};
|
130
129
|
|
131
|
-
private _sendTrackingEvent = (event: TrackingEvent, payload
|
130
|
+
private _sendTrackingEvent = (event: TrackingEvent, payload?: TrackingPayload) => {
|
131
|
+
const vouchId = this._findVouchId();
|
132
|
+
|
133
|
+
if (!vouchId || this.host.disableTracking) {
|
134
|
+
return;
|
135
|
+
}
|
136
|
+
|
132
137
|
const { publicApiUrl } = getEnvUrls(this.host.env);
|
133
138
|
const { client, tab, request, visitor } = this._getUids();
|
134
139
|
|
135
|
-
// Don't send tracking if we don't have a source
|
136
140
|
navigator.sendBeacon(
|
137
141
|
`${publicApiUrl}/api/events`,
|
138
142
|
JSON.stringify({
|
139
143
|
event,
|
140
|
-
payload
|
144
|
+
payload: {
|
145
|
+
vouchId,
|
146
|
+
...payload
|
147
|
+
},
|
141
148
|
context: {
|
142
149
|
'x-uid-client': client,
|
143
150
|
'x-uid-tab': tab,
|
@@ -156,87 +163,74 @@ class TrackingController implements ReactiveController {
|
|
156
163
|
|
157
164
|
// Only send loaded event once per session
|
158
165
|
if (!this._hasLoaded[vouchId]) {
|
159
|
-
this._sendTrackingEvent('VOUCH_LOADED'
|
160
|
-
vouchId
|
161
|
-
});
|
166
|
+
this._sendTrackingEvent('VOUCH_LOADED');
|
162
167
|
this._hasLoaded[vouchId] = true;
|
163
168
|
}
|
164
169
|
};
|
165
170
|
|
166
171
|
private _handlePlay = () => {
|
167
|
-
const vouchId = this._findVouchId();
|
168
|
-
|
169
|
-
if (!vouchId) {
|
170
|
-
return;
|
171
|
-
}
|
172
|
-
|
173
172
|
// Only send the video played event once per session
|
174
173
|
if (!this._hasPlayed) {
|
175
174
|
this._sendTrackingEvent('VIDEO_PLAYED', {
|
176
|
-
vouchId,
|
177
175
|
streamStart: this.host.currentTime
|
178
176
|
});
|
179
177
|
this._hasPlayed = true;
|
180
178
|
}
|
181
179
|
};
|
182
180
|
|
183
|
-
private _handleVideoPlay = ({ detail: { id, node } }: CustomEvent<VideoEventDetail>) => {
|
184
|
-
const vouchId = this._findVouchId();
|
185
|
-
if (!vouchId) {
|
186
|
-
return;
|
187
|
-
}
|
181
|
+
private _handleVideoPlay = ({ detail: { id, key, node } }: CustomEvent<VideoEventDetail>) => {
|
188
182
|
// Only increment play count once per session
|
189
|
-
if (!this._answersViewed[
|
183
|
+
if (!this._answersViewed[key]) {
|
190
184
|
this._sendTrackingEvent('VOUCH_RESPONSE_VIEWED', {
|
191
|
-
vouchId,
|
192
185
|
answerId: id
|
193
186
|
});
|
194
|
-
this._answersViewed[
|
187
|
+
this._answersViewed[key] = true;
|
195
188
|
}
|
196
|
-
|
197
|
-
this.
|
189
|
+
|
190
|
+
this._streamStartTime[key] = node.currentTime;
|
191
|
+
this._streamLatestTime[key] = node.currentTime;
|
198
192
|
};
|
199
193
|
|
200
|
-
private _handleVideoTimeUpdate = ({ detail: { id, node } }: CustomEvent<VideoEventDetail>) => {
|
201
|
-
|
202
|
-
if (!
|
203
|
-
|
194
|
+
private _handleVideoTimeUpdate = ({ detail: { id, key, node } }: CustomEvent<VideoEventDetail>) => {
|
195
|
+
// We only want to count any time that the video is actually playing
|
196
|
+
if (!this.host.paused) {
|
197
|
+
this._currentlyPlayingVideo = { id, key, node };
|
198
|
+
this._streamLatestTime[key] = node.currentTime;
|
204
199
|
}
|
205
|
-
|
200
|
+
|
206
201
|
if (
|
202
|
+
!node.paused &&
|
207
203
|
!this.host.paused &&
|
208
204
|
// Only fire the video seeked event when this video is the active one
|
209
205
|
id === this.host.scene?.video?.id &&
|
210
206
|
// Throttle the frequency that we send streamed events while playing
|
211
|
-
|
207
|
+
this._streamLatestTime[key] - this._streamStartTime[key] > STREAMED_THROTTLE
|
212
208
|
) {
|
213
209
|
this._sendTrackingEvent('VIDEO_STREAMED', {
|
214
|
-
vouchId,
|
215
210
|
answerId: id,
|
216
|
-
streamStart: this.
|
217
|
-
streamEnd:
|
211
|
+
streamStart: this._streamStartTime[key],
|
212
|
+
streamEnd: this._streamLatestTime[key]
|
218
213
|
});
|
219
|
-
|
220
|
-
this.
|
214
|
+
|
215
|
+
this._streamStartTime[key] = node.currentTime;
|
221
216
|
}
|
222
217
|
};
|
223
218
|
|
224
|
-
private _handleVideoPause = ({ detail: { id,
|
225
|
-
|
226
|
-
if (
|
227
|
-
|
219
|
+
private _handleVideoPause = ({ detail: { id, key } }: CustomEvent<VideoEventDetail>) => {
|
220
|
+
// Don't send a tracking event when seeking backwards
|
221
|
+
if (this._streamLatestTime[key] > this._streamStartTime[key]) {
|
222
|
+
// Send a video streamed event any time the video pauses then reset the streamed state
|
223
|
+
// We do this to capture the last bit of time that the video was played between the previous
|
224
|
+
// stream event and the video being paused manually or stopping because it ended
|
225
|
+
this._sendTrackingEvent('VIDEO_STREAMED', {
|
226
|
+
answerId: id,
|
227
|
+
streamStart: this._streamStartTime[key],
|
228
|
+
streamEnd: this._streamLatestTime[key]
|
229
|
+
});
|
228
230
|
}
|
229
|
-
|
230
|
-
|
231
|
-
|
232
|
-
this._sendTrackingEvent('VIDEO_STREAMED', {
|
233
|
-
vouchId,
|
234
|
-
answerId: id,
|
235
|
-
streamStart: this._streamedTime[id],
|
236
|
-
streamEnd: node.currentTime
|
237
|
-
});
|
238
|
-
delete this._streamedTime[id];
|
239
|
-
delete this._streamedPrevTimestamp[id];
|
231
|
+
this._currentlyPlayingVideo = null;
|
232
|
+
delete this._streamStartTime[key];
|
233
|
+
delete this._streamLatestTime[key];
|
240
234
|
};
|
241
235
|
|
242
236
|
hostConnected() {
|
@@ -250,6 +244,20 @@ class TrackingController implements ReactiveController {
|
|
250
244
|
}
|
251
245
|
|
252
246
|
hostDisconnected() {
|
247
|
+
if (this._currentlyPlayingVideo) {
|
248
|
+
const { id, key } = this._currentlyPlayingVideo;
|
249
|
+
if (this._streamLatestTime[key] > this._streamStartTime[key]) {
|
250
|
+
// Send a video streamed event any time the video pauses then reset the streamed state
|
251
|
+
// We do this to capture the last bit of time that the video was played between the previous
|
252
|
+
// stream event and the video being paused manually or stopping because it ended
|
253
|
+
this._sendTrackingEvent('VIDEO_STREAMED', {
|
254
|
+
answerId: id,
|
255
|
+
streamStart: this._streamStartTime[key],
|
256
|
+
streamEnd: this._streamLatestTime[key]
|
257
|
+
});
|
258
|
+
}
|
259
|
+
}
|
260
|
+
|
253
261
|
this.host.removeEventListener('vouch:loaded', this._handleVouchLoaded);
|
254
262
|
this.host.mediaPlayer?.removeEventListener('play', this._handlePlay);
|
255
263
|
this.host.mediaPlayer?.removeEventListener('video:play', this._handleVideoPlay);
|
@@ -17,6 +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
|
+
disableTracking?: boolean;
|
20
21
|
trackingSource?: string;
|
21
22
|
vouchId?: string;
|
22
23
|
templateId?: string;
|
@@ -32,6 +33,7 @@ class Embed extends LitElement {
|
|
32
33
|
|
33
34
|
@property({ type: String }) env: EmbedProps['env'] = 'prod';
|
34
35
|
@property({ type: String }) apiKey: EmbedProps['apiKey'] = '';
|
36
|
+
@property({ type: Boolean }) disableTracking: EmbedProps['disableTracking'] = false;
|
35
37
|
@property({ type: String }) trackingSource: EmbedProps['trackingSource'] = 'embed';
|
36
38
|
|
37
39
|
@property({ type: Array }) controls: EmbedProps['controls'];
|
@@ -57,6 +59,7 @@ class Embed extends LitElement {
|
|
57
59
|
'waiting',
|
58
60
|
|
59
61
|
'video:loadeddata',
|
62
|
+
'video:seeking',
|
60
63
|
'video:seeked',
|
61
64
|
'video:play',
|
62
65
|
'video:playing',
|