@vouchfor/embeds 0.0.0-experiment.2a3ebd2 → 0.0.0-experiment.2c20f03

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.
Files changed (33) hide show
  1. package/dist/es/embeds.js +977 -1425
  2. package/dist/es/embeds.js.map +1 -1
  3. package/dist/es/src/components/DialogEmbed/DialogOverlay.d.ts +20 -0
  4. package/dist/es/src/components/DialogEmbed/DialogPortal.d.ts +36 -0
  5. package/dist/es/src/components/DialogEmbed/index.d.ts +38 -0
  6. package/dist/es/src/components/PlayerEmbed/controllers/event-forwarder.d.ts +15 -0
  7. package/dist/es/src/components/{Embed → PlayerEmbed}/controllers/fetcher.d.ts +5 -4
  8. package/dist/es/src/components/{Embed/controllers/tracking.d.ts → PlayerEmbed/controllers/tracking/index.d.ts} +14 -11
  9. package/dist/es/src/components/PlayerEmbed/controllers/tracking/utils.d.ts +17 -0
  10. package/dist/es/src/components/PlayerEmbed/index.d.ts +75 -0
  11. package/dist/es/src/components/PlayerEmbed/tests/data.d.ts +3 -0
  12. package/dist/es/src/index.d.ts +2 -1
  13. package/dist/iife/dialog-embed/embed.iife.js +1759 -0
  14. package/dist/iife/dialog-embed/embed.iife.js.map +1 -0
  15. package/dist/iife/embeds.iife.js +771 -446
  16. package/dist/iife/embeds.iife.js.map +1 -1
  17. package/dist/iife/player-embed/embed.iife.js +1621 -0
  18. package/dist/iife/player-embed/embed.iife.js.map +1 -0
  19. package/package.json +43 -31
  20. package/src/components/DialogEmbed/Dialog.stories.ts +91 -0
  21. package/src/components/DialogEmbed/DialogOverlay.ts +131 -0
  22. package/src/components/DialogEmbed/DialogPortal.ts +126 -0
  23. package/src/components/DialogEmbed/index.ts +97 -0
  24. package/src/components/{Embed/Embed.stories.ts → PlayerEmbed/PlayerEmbed.stories.ts} +42 -15
  25. package/src/components/{Embed → PlayerEmbed}/controllers/event-forwarder.ts +6 -5
  26. package/src/components/{Embed → PlayerEmbed}/controllers/fetcher.ts +33 -14
  27. package/src/components/{Embed/controllers/tracking.ts → PlayerEmbed/controllers/tracking/index.ts} +47 -115
  28. package/src/components/PlayerEmbed/controllers/tracking/utils.ts +95 -0
  29. package/src/components/{Embed → PlayerEmbed}/index.ts +82 -28
  30. package/src/components/PlayerEmbed/tests/PlayerEmbed.spec.ts +80 -0
  31. package/src/components/PlayerEmbed/tests/data.ts +183 -0
  32. package/src/index.ts +2 -1
  33. package/dist/es/src/components/Embed/index.d.ts +0 -66
@@ -1,27 +1,28 @@
1
1
  import { createRef, ref } from 'lit/directives/ref.js';
2
2
 
3
- import type { Embed } from '..';
3
+ import type { PlayerEmbed } from '..';
4
4
  import type { ReactiveController, ReactiveControllerHost } from 'lit';
5
+ import type { DirectiveResult } from 'lit/directive.js';
5
6
  import type { Ref } from 'lit/directives/ref.js';
6
7
 
7
8
  import { forwardEvent } from '~/utils/events';
8
9
 
9
- type EmbedHost = ReactiveControllerHost & Embed;
10
+ type PlayerEmbedHost = ReactiveControllerHost & PlayerEmbed;
10
11
 
11
12
  class EventForwardController implements ReactiveController {
12
- host: EmbedHost;
13
+ host: PlayerEmbedHost;
13
14
 
14
15
  private _events: string[] = [];
15
16
  private _cleanup: (() => void)[] = [];
16
17
  private _forwardElementRef: Ref<HTMLElement> = createRef();
17
18
 
18
- constructor(host: EmbedHost, events: string[]) {
19
+ constructor(host: PlayerEmbedHost, events: string[]) {
19
20
  this.host = host;
20
21
  this._events = events;
21
22
  host.addController(this);
22
23
  }
23
24
 
24
- register() {
25
+ register(): DirectiveResult {
25
26
  return ref(this._forwardElementRef);
26
27
  }
27
28
 
@@ -1,26 +1,29 @@
1
1
  import { Task } from '@lit/task';
2
2
  import { v4 as uuidv4 } from 'uuid';
3
3
 
4
- import type { Embed, EmbedProps } from '..';
4
+ import type { PlayerEmbed, PlayerEmbedProps } from '..';
5
5
  import type { ReactiveControllerHost } from 'lit';
6
6
  import type { Environment } from '~/utils/env';
7
7
 
8
8
  import { getEnvUrls } from '~/utils/env';
9
9
 
10
- type EmbedHost = ReactiveControllerHost & Embed;
10
+ type PlayerEmbedHost = ReactiveControllerHost & PlayerEmbed;
11
11
 
12
- type TaskDeps = [
13
- EmbedProps['env'],
14
- EmbedProps['apiKey'],
15
- EmbedProps['data'],
16
- EmbedProps['vouchId'],
17
- EmbedProps['templateId']
12
+ type FetchTaskDeps = [
13
+ PlayerEmbedProps['env'],
14
+ PlayerEmbedProps['apiKey'],
15
+ PlayerEmbedProps['data'],
16
+ PlayerEmbedProps['vouchId'],
17
+ PlayerEmbedProps['templateId']
18
18
  ];
19
19
 
20
+ type FilterTaskDeps = [PlayerEmbedProps['data'], PlayerEmbedProps['questions']];
21
+
20
22
  class FetcherController {
21
- host: EmbedHost;
23
+ host: PlayerEmbedHost;
22
24
 
23
25
  private _fetching = false;
26
+ private _vouch: PlayerEmbedProps['data'];
24
27
 
25
28
  set fetching(value) {
26
29
  if (this._fetching !== value) {
@@ -94,11 +97,11 @@ class FetcherController {
94
97
  return template;
95
98
  };
96
99
 
97
- constructor(host: EmbedHost) {
100
+ constructor(host: PlayerEmbedHost) {
98
101
  this.host = host;
99
- new Task<TaskDeps, void>(
102
+ new Task<FetchTaskDeps, void>(
100
103
  this.host,
101
- async ([env, apiKey, data, vouchId, templateId]: TaskDeps) => {
104
+ async ([env, apiKey, data, vouchId, templateId]: FetchTaskDeps) => {
102
105
  try {
103
106
  host.vouch = undefined;
104
107
  host.template = undefined;
@@ -109,7 +112,7 @@ class FetcherController {
109
112
  this.fetching = true;
110
113
  template = await this.getTemplate(env, apiKey, templateId);
111
114
  }
112
- host.vouch = data;
115
+ this._vouch = data;
113
116
  host.template = template ?? data?.settings?.template?.instance;
114
117
  } else if (vouchId) {
115
118
  this.fetching = true;
@@ -118,7 +121,7 @@ class FetcherController {
118
121
  this.getVouch(env, apiKey, vouchId),
119
122
  templateId ? this.getTemplate(env, apiKey, templateId) : null
120
123
  ]);
121
- host.vouch = vouch;
124
+ this._vouch = vouch;
122
125
  host.template = template ?? vouch?.settings?.template?.instance;
123
126
  }
124
127
  } finally {
@@ -127,6 +130,22 @@ class FetcherController {
127
130
  },
128
131
  () => [host.env, host.apiKey, host.data, host.vouchId, host.templateId]
129
132
  );
133
+
134
+ // This second task is to be able to filter the vouch without fetching it again if only the questions changed
135
+ new Task<FilterTaskDeps, void>(
136
+ this.host,
137
+ ([vouch, questions]: FilterTaskDeps) => {
138
+ host.vouch = vouch
139
+ ? {
140
+ ...vouch,
141
+ questions: {
142
+ items: vouch?.questions.items.filter((_, index) => !questions?.length || questions?.includes(index + 1))
143
+ }
144
+ }
145
+ : undefined;
146
+ },
147
+ () => [this._vouch, host.questions]
148
+ );
130
149
  }
131
150
  }
132
151
 
@@ -1,15 +1,13 @@
1
- import { v4 as uuidv4 } from 'uuid';
2
-
3
- import type { Embed } from '..';
1
+ import type { PlayerEmbed } from '../..';
4
2
  import type { VideoEventDetail } from '@vouchfor/media-player';
5
3
  import type { ReactiveController, ReactiveControllerHost } from 'lit';
6
4
 
7
- import packageJson from '../../../../package.json';
5
+ import { findVouchId, getReportingMetadata, getUids } from './utils';
8
6
  import { getEnvUrls } from '~/utils/env';
9
7
 
10
8
  const MINIMUM_SEND_THRESHOLD = 1;
11
9
 
12
- type EmbedHost = ReactiveControllerHost & Embed;
10
+ type PlayerEmbedHost = ReactiveControllerHost & PlayerEmbed;
13
11
 
14
12
  type TrackingEvent = 'VOUCH_LOADED' | 'VOUCH_RESPONSE_VIEWED' | 'VIDEO_PLAYED' | 'VIDEO_STREAMED';
15
13
  type TrackingPayload = {
@@ -19,6 +17,13 @@ type TrackingPayload = {
19
17
  streamEnd?: number;
20
18
  };
21
19
 
20
+ type BatchEvent = {
21
+ event: TrackingEvent;
22
+ payload: TrackingPayload & {
23
+ time: string;
24
+ };
25
+ };
26
+
22
27
  type TimeMap = {
23
28
  [key: string]: number;
24
29
  };
@@ -28,12 +33,9 @@ type BooleanMap = {
28
33
  };
29
34
 
30
35
  class TrackingController implements ReactiveController {
31
- host: EmbedHost;
32
-
33
- private _tabId: string | undefined = undefined;
34
- private _clientId: string | undefined = undefined;
35
- private _visitorId: string | undefined = undefined;
36
+ host: PlayerEmbedHost;
36
37
 
38
+ private _batchedEvents: BatchEvent[] = [];
37
39
  private _hasPlayed = false;
38
40
  private _hasLoaded: BooleanMap = {};
39
41
  private _answersViewed: BooleanMap = {};
@@ -41,124 +43,53 @@ class TrackingController implements ReactiveController {
41
43
  private _streamLatestTime: TimeMap = {};
42
44
  private _currentlyPlayingVideo: VideoEventDetail | null = null;
43
45
 
44
- constructor(host: EmbedHost) {
46
+ constructor(host: PlayerEmbedHost) {
45
47
  this.host = host;
46
48
  host.addController(this);
47
49
  }
48
50
 
49
- private _findVouchId(payload?: TrackingPayload) {
50
- if (payload && 'vouchId' in payload) {
51
- return payload.vouchId;
52
- }
53
- if (this.host.vouch) {
54
- return this.host.vouch.id;
55
- }
56
- return null;
57
- }
58
-
59
- private _createVisitor = (visitorId: string) => {
60
- const { publicApiUrl } = getEnvUrls(this.host.env);
61
- window.localStorage?.setItem?.('vouch-uid-visitor', visitorId);
62
- navigator.sendBeacon(`${publicApiUrl}/api/visitor`, JSON.stringify({ visitorId }));
63
- return visitorId;
64
- };
65
-
66
- private _getUids() {
67
- if (typeof window === 'undefined') {
68
- return {
69
- client: null,
70
- tab: null,
71
- request: uuidv4()
72
- };
73
- }
74
-
75
- // Persisted for a user for the same device + browser, so we can e.g. search for all logs related to that browser
76
- const visitorId =
77
- this._visitorId || window.localStorage?.getItem?.('vouch-uid-visitor') || this._createVisitor(uuidv4());
78
- // Persisted for a user for the same device + browser, so we can e.g. search for all logs related to that browser
79
- const clientId = this._clientId || window.localStorage?.getItem?.('vouch-uid-client') || uuidv4();
80
- // Persisted in session storage, so we can search for everything the user has done in a specific tab
81
- const tabId = this._tabId || window.sessionStorage?.getItem?.('vouch-uid-tab') || uuidv4();
82
- // Not persisted, allows us to search for any logs related to a single FE request
83
- // E.g. BE should pass this request ID through all other services to be able to group logs
84
- const requestId = uuidv4();
85
-
86
- // Cache and persist uids
87
- if (visitorId !== this._visitorId) {
88
- this._visitorId = visitorId;
89
- window.localStorage?.setItem?.('vouch-uid-visitor', visitorId);
90
- }
91
-
92
- if (clientId !== this._clientId) {
93
- this._clientId = clientId;
94
- window.localStorage?.setItem?.('vouch-uid-client', clientId);
95
- }
51
+ private _createTrackingEvent = (event: TrackingEvent, payload?: TrackingPayload) => {
52
+ const vouchId = findVouchId(payload, this.host.vouch);
96
53
 
97
- if (tabId !== this._tabId) {
98
- this._tabId = tabId;
99
- window.sessionStorage?.setItem?.('vouch-uid-tab', tabId);
54
+ if (!vouchId || this.host.disableTracking) {
55
+ return;
100
56
  }
101
57
 
102
- return {
103
- client: clientId,
104
- tab: tabId,
105
- request: requestId,
106
- visitor: visitorId
107
- };
108
- }
109
-
110
- private _getReportingMetadata = () => {
111
- const [country, region] = Intl.DateTimeFormat().resolvedOptions().timeZone?.split?.('/') ?? [];
112
-
113
- const utmParams: any = {};
114
- [...new URLSearchParams(location.search).entries()].forEach(([key, value]) => {
115
- if (/utm/.test(key)) {
116
- const param = key.toLowerCase().replace(/[-_][a-z0-9]/g, (group) => group.slice(-1).toUpperCase());
117
- utmParams[param] = value;
58
+ this._batchedEvents.push({
59
+ event,
60
+ payload: {
61
+ ...payload,
62
+ vouchId,
63
+ time: new Date().toISOString()
118
64
  }
119
65
  });
120
-
121
- return {
122
- source: this.host.trackingSource,
123
- time: new Date(),
124
- region,
125
- country,
126
- screenHeight: window.screen.height,
127
- screenWidth: window.screen.width,
128
- referrer: document.referrer,
129
- currentUrl: location.href,
130
- ...utmParams
131
- };
132
66
  };
133
67
 
134
- private _sendTrackingEvent = (event: TrackingEvent, payload?: TrackingPayload) => {
135
- const vouchId = this._findVouchId(payload);
136
-
137
- if (!vouchId || this.host.disableTracking) {
68
+ private _sendTrackingEvent = () => {
69
+ if (this._batchedEvents.length <= 0) {
138
70
  return;
139
71
  }
140
72
 
141
73
  const { publicApiUrl } = getEnvUrls(this.host.env);
142
- const { client, tab, request, visitor } = this._getUids();
74
+ const { client, tab, request, visitor } = getUids(this.host.env);
143
75
 
144
76
  navigator.sendBeacon(
145
- `${publicApiUrl}/api/v2/events`,
77
+ `${publicApiUrl}/api/batchevents`,
146
78
  JSON.stringify({
147
- event,
148
79
  payload: {
149
- ...payload,
150
- vouchId
80
+ events: this._batchedEvents
151
81
  },
152
82
  context: {
153
83
  'x-uid-client': client,
154
84
  'x-uid-tab': tab,
155
85
  'x-uid-request': request,
156
86
  'x-uid-visitor': visitor,
157
- 'x-reporting-metadata': this._getReportingMetadata(),
158
- 'x-embeds-version': packageJson.version
87
+ 'x-reporting-metadata': getReportingMetadata(this.host.trackingSource)
159
88
  }
160
89
  })
161
90
  );
91
+
92
+ this._batchedEvents = [];
162
93
  };
163
94
 
164
95
  private _streamEnded = () => {
@@ -168,7 +99,7 @@ class TrackingController implements ReactiveController {
168
99
  if (this._streamLatestTime[key] > this._streamStartTime[key] + MINIMUM_SEND_THRESHOLD) {
169
100
  // Send a video streamed event any time the stream ends to capture the time between starting
170
101
  // the video and the video stopping for any reason (pausing, deleting the embed node or closing the browser)
171
- this._sendTrackingEvent('VIDEO_STREAMED', {
102
+ this._createTrackingEvent('VIDEO_STREAMED', {
172
103
  answerId: id,
173
104
  streamStart: this._streamStartTime[key],
174
105
  streamEnd: this._streamLatestTime[key]
@@ -188,7 +119,7 @@ class TrackingController implements ReactiveController {
188
119
 
189
120
  // Only send loaded event once per session
190
121
  if (!this._hasLoaded[vouchId]) {
191
- this._sendTrackingEvent('VOUCH_LOADED', { vouchId });
122
+ this._createTrackingEvent('VOUCH_LOADED', { vouchId });
192
123
  this._hasLoaded[vouchId] = true;
193
124
  }
194
125
  };
@@ -196,26 +127,21 @@ class TrackingController implements ReactiveController {
196
127
  private _handlePlay = () => {
197
128
  // Only send the video played event once per session
198
129
  if (!this._hasPlayed) {
199
- this._sendTrackingEvent('VIDEO_PLAYED', {
130
+ this._createTrackingEvent('VIDEO_PLAYED', {
200
131
  streamStart: this.host.currentTime
201
132
  });
202
133
  this._hasPlayed = true;
203
134
  }
204
135
  };
205
136
 
206
- private _handleVideoPlay = ({ detail: { id, key, node } }: CustomEvent<VideoEventDetail>) => {
137
+ private _handleVideoPlay = ({ detail: { id, key } }: CustomEvent<VideoEventDetail>) => {
207
138
  // Only increment play count once per session
208
139
  if (!this._answersViewed[key]) {
209
- this._sendTrackingEvent('VOUCH_RESPONSE_VIEWED', {
140
+ this._createTrackingEvent('VOUCH_RESPONSE_VIEWED', {
210
141
  answerId: id
211
142
  });
212
143
  this._answersViewed[key] = true;
213
144
  }
214
-
215
- if (!this._streamStartTime[key]) {
216
- this._streamStartTime[key] = node.currentTime;
217
- this._streamLatestTime[key] = node.currentTime;
218
- }
219
145
  };
220
146
 
221
147
  private _handleVideoTimeUpdate = ({ detail: { id, key, node } }: CustomEvent<VideoEventDetail>) => {
@@ -227,12 +153,17 @@ class TrackingController implements ReactiveController {
227
153
  ) {
228
154
  this._currentlyPlayingVideo = { id, key, node };
229
155
  this._streamLatestTime[key] = node.currentTime;
156
+
157
+ if (!this._streamStartTime[key]) {
158
+ this._streamStartTime[key] = node.currentTime;
159
+ this._streamLatestTime[key] = node.currentTime;
160
+ }
230
161
  }
231
162
  };
232
163
 
233
164
  private _handleVideoPause = ({ detail: { id, key } }: CustomEvent<VideoEventDetail>) => {
234
165
  if (this._streamLatestTime[key] > this._streamStartTime[key] + MINIMUM_SEND_THRESHOLD) {
235
- this._sendTrackingEvent('VIDEO_STREAMED', {
166
+ this._createTrackingEvent('VIDEO_STREAMED', {
236
167
  answerId: id,
237
168
  streamStart: this._streamStartTime[key],
238
169
  streamEnd: this._streamLatestTime[key]
@@ -244,9 +175,7 @@ class TrackingController implements ReactiveController {
244
175
 
245
176
  private _pageUnloading = () => {
246
177
  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();
178
+ this._sendTrackingEvent();
250
179
  };
251
180
 
252
181
  private _handleVisibilityChange = () => {
@@ -275,7 +204,9 @@ class TrackingController implements ReactiveController {
275
204
  }
276
205
 
277
206
  hostDisconnected() {
278
- this._streamEnded();
207
+ // Send events if DOM node is destroyed
208
+ this._pageUnloading();
209
+
279
210
  if ('onvisibilitychange' in document) {
280
211
  document.removeEventListener('visibilitychange', this._handleVisibilityChange);
281
212
  } else {
@@ -290,3 +221,4 @@ class TrackingController implements ReactiveController {
290
221
  }
291
222
 
292
223
  export { TrackingController };
224
+ export type { TrackingEvent, TrackingPayload };
@@ -0,0 +1,95 @@
1
+ import { TEMPLATE_VERSION } from '@vouchfor/canvas-video';
2
+ import { v4 as uuidv4 } from 'uuid';
3
+
4
+ import type { TrackingPayload } from '.';
5
+ import type { Vouch } from '@vouchfor/video-utils';
6
+ import type { Environment } from '~/utils/env';
7
+
8
+ import packageJson from '~/../package.json';
9
+ import { getEnvUrls } from '~/utils/env';
10
+
11
+ function createVisitor(env: Environment) {
12
+ const { publicApiUrl } = getEnvUrls(env);
13
+ const visitorId = uuidv4();
14
+ navigator.sendBeacon(`${publicApiUrl}/api/visitor`, JSON.stringify({ visitorId }));
15
+ return visitorId;
16
+ }
17
+
18
+ function getUids(env: Environment) {
19
+ if (typeof window === 'undefined') {
20
+ return {
21
+ client: null,
22
+ tab: null,
23
+ request: uuidv4()
24
+ };
25
+ }
26
+
27
+ // Persisted for a user for the same device + browser, so we can e.g. search for all logs related to that browser
28
+ let visitorId = window.localStorage?.getItem?.('vouch-uid-visitor');
29
+ // Persisted for a user for the same device + browser, so we can e.g. search for all logs related to that browser
30
+ let clientId = window.localStorage?.getItem?.('vouch-uid-client');
31
+ // Persisted in session storage, so we can search for everything the user has done in a specific tab
32
+ let tabId = window.sessionStorage?.getItem?.('vouch-uid-tab');
33
+ // Not persisted, allows us to search for any logs related to a single FE request
34
+ // E.g. BE should pass this request ID through all other services to be able to group logs
35
+ const requestId = uuidv4();
36
+
37
+ // Cache uids
38
+ if (!visitorId) {
39
+ visitorId = createVisitor(env);
40
+ window.localStorage?.setItem?.('vouch-uid-visitor', visitorId);
41
+ }
42
+
43
+ if (!clientId) {
44
+ clientId = uuidv4();
45
+ window.localStorage?.setItem?.('vouch-uid-client', clientId);
46
+ }
47
+
48
+ if (!tabId) {
49
+ tabId = uuidv4();
50
+ window.sessionStorage?.setItem?.('vouch-uid-tab', tabId);
51
+ }
52
+
53
+ return {
54
+ client: clientId,
55
+ tab: tabId,
56
+ request: requestId,
57
+ visitor: visitorId
58
+ };
59
+ }
60
+
61
+ function findVouchId(payload?: TrackingPayload, vouch?: Vouch) {
62
+ if (payload && 'vouchId' in payload) {
63
+ return payload.vouchId;
64
+ }
65
+ return vouch?.id ?? null;
66
+ }
67
+
68
+ function getReportingMetadata(source = 'embedded_player') {
69
+ const [country, region] = Intl.DateTimeFormat().resolvedOptions().timeZone?.split?.('/') ?? [];
70
+
71
+ const utmParams: any = {};
72
+ [...new URLSearchParams(location.search).entries()].forEach(([key, value]) => {
73
+ if (/utm/.test(key)) {
74
+ const param = key.toLowerCase().replace(/[-_][a-z0-9]/g, (group) => group.slice(-1).toUpperCase());
75
+ utmParams[param] = value;
76
+ }
77
+ });
78
+
79
+ return {
80
+ source,
81
+ time: new Date(),
82
+ region,
83
+ country,
84
+ screenHeight: window.screen.height,
85
+ screenWidth: window.screen.width,
86
+ referrer: document.referrer,
87
+ currentUrl: location.href,
88
+ embedType: 'media-player-embed',
89
+ embedVersion: packageJson.version,
90
+ templateVersion: TEMPLATE_VERSION,
91
+ ...utmParams
92
+ };
93
+ }
94
+
95
+ export { getUids, findVouchId, getReportingMetadata };