@vouchfor/embeds 0.0.0-experiment.30b1361 → 0.0.0-experiment.349f4f2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (72) hide show
  1. package/dist/es/browser-7226997c.js +733 -0
  2. package/dist/es/browser-7226997c.js.map +1 -0
  3. package/dist/es/embeds.js +12 -1610
  4. package/dist/es/embeds.js.map +1 -1
  5. package/dist/es/index-4dd3a66e.js +16482 -0
  6. package/dist/es/index-4dd3a66e.js.map +1 -0
  7. package/dist/es/src/components/DialogEmbed/DialogOverlay.d.ts +19 -0
  8. package/dist/es/src/components/DialogEmbed/DialogPortal.d.ts +35 -0
  9. package/dist/es/src/components/DialogEmbed/index.d.ts +36 -0
  10. package/dist/es/src/components/PlayerEmbed/controllers/event-forwarder.d.ts +15 -0
  11. package/dist/es/src/components/{Embed → PlayerEmbed}/controllers/fetcher.d.ts +4 -4
  12. package/dist/es/src/components/{Embed/controllers/tracking.d.ts → PlayerEmbed/controllers/tracking/index.d.ts} +14 -11
  13. package/dist/es/src/components/PlayerEmbed/controllers/tracking/utils.d.ts +17 -0
  14. package/dist/es/src/components/{Embed → PlayerEmbed}/index.d.ts +24 -20
  15. package/dist/es/src/components/PlayerEmbed/tests/data.d.ts +3 -0
  16. package/dist/es/src/index.d.ts +2 -1
  17. package/dist/iife/dialog-embed/browser-0f71b138.js +433 -0
  18. package/dist/iife/dialog-embed/browser-0f71b138.js.map +1 -0
  19. package/dist/iife/dialog-embed/embed.iife.js +1722 -0
  20. package/dist/iife/dialog-embed/embed.iife.js.map +1 -0
  21. package/dist/iife/dialog-embed/embed.js +5 -0
  22. package/dist/iife/dialog-embed/embed.js.map +1 -0
  23. package/dist/iife/dialog-embed/index-b3521ce3.js +33265 -0
  24. package/dist/iife/dialog-embed/index-b3521ce3.js.map +1 -0
  25. package/dist/iife/dialog-embed/src/components/DialogEmbed/DialogOverlay.d.ts +19 -0
  26. package/dist/iife/dialog-embed/src/components/DialogEmbed/DialogPortal.d.ts +35 -0
  27. package/dist/iife/dialog-embed/src/components/DialogEmbed/index.d.ts +36 -0
  28. package/dist/iife/dialog-embed/src/components/PlayerEmbed/controllers/event-forwarder.d.ts +15 -0
  29. package/dist/iife/dialog-embed/src/components/PlayerEmbed/controllers/fetcher.d.ts +14 -0
  30. package/dist/iife/dialog-embed/src/components/PlayerEmbed/controllers/tracking/index.d.ts +36 -0
  31. package/dist/iife/dialog-embed/src/components/PlayerEmbed/controllers/tracking/utils.d.ts +17 -0
  32. package/dist/iife/dialog-embed/src/components/PlayerEmbed/index.d.ts +73 -0
  33. package/dist/iife/dialog-embed/src/components/PlayerEmbed/tests/data.d.ts +3 -0
  34. package/dist/iife/dialog-embed/src/index.d.ts +2 -0
  35. package/dist/iife/dialog-embed/src/utils/env.d.ts +12 -0
  36. package/dist/iife/dialog-embed/src/utils/events.d.ts +2 -0
  37. package/dist/iife/embeds.iife.js +531 -354
  38. package/dist/iife/embeds.iife.js.map +1 -1
  39. package/dist/iife/player-embed/browser-024de75a.js +433 -0
  40. package/dist/iife/player-embed/browser-024de75a.js.map +1 -0
  41. package/dist/iife/player-embed/embed.iife.js +1584 -0
  42. package/dist/iife/player-embed/embed.iife.js.map +1 -0
  43. package/dist/iife/player-embed/embed.js +5 -0
  44. package/dist/iife/player-embed/embed.js.map +1 -0
  45. package/dist/iife/player-embed/index-f6f7cae0.js +32820 -0
  46. package/dist/iife/player-embed/index-f6f7cae0.js.map +1 -0
  47. package/dist/iife/player-embed/src/components/DialogEmbed/DialogOverlay.d.ts +19 -0
  48. package/dist/iife/player-embed/src/components/DialogEmbed/DialogPortal.d.ts +35 -0
  49. package/dist/iife/player-embed/src/components/DialogEmbed/index.d.ts +36 -0
  50. package/dist/iife/player-embed/src/components/PlayerEmbed/controllers/event-forwarder.d.ts +15 -0
  51. package/dist/iife/player-embed/src/components/PlayerEmbed/controllers/fetcher.d.ts +14 -0
  52. package/dist/iife/player-embed/src/components/PlayerEmbed/controllers/tracking/index.d.ts +36 -0
  53. package/dist/iife/player-embed/src/components/PlayerEmbed/controllers/tracking/utils.d.ts +17 -0
  54. package/dist/iife/player-embed/src/components/PlayerEmbed/index.d.ts +73 -0
  55. package/dist/iife/player-embed/src/components/PlayerEmbed/tests/data.d.ts +3 -0
  56. package/dist/iife/player-embed/src/index.d.ts +2 -0
  57. package/dist/iife/player-embed/src/utils/env.d.ts +12 -0
  58. package/dist/iife/player-embed/src/utils/events.d.ts +2 -0
  59. package/package.json +13 -5
  60. package/src/components/DialogEmbed/Dialog.stories.ts +91 -0
  61. package/src/components/DialogEmbed/DialogOverlay.ts +131 -0
  62. package/src/components/DialogEmbed/DialogPortal.ts +126 -0
  63. package/src/components/DialogEmbed/index.ts +97 -0
  64. package/src/components/{Embed/Embed.stories.ts → PlayerEmbed/PlayerEmbed.stories.ts} +15 -15
  65. package/src/components/{Embed → PlayerEmbed}/controllers/event-forwarder.ts +6 -5
  66. package/src/components/{Embed → PlayerEmbed}/controllers/fetcher.ts +11 -11
  67. package/src/components/{Embed/controllers/tracking.ts → PlayerEmbed/controllers/tracking/index.ts} +47 -115
  68. package/src/components/PlayerEmbed/controllers/tracking/utils.ts +95 -0
  69. package/src/components/{Embed → PlayerEmbed}/index.ts +69 -28
  70. package/src/components/PlayerEmbed/tests/PlayerEmbed.spec.ts +80 -0
  71. package/src/components/PlayerEmbed/tests/data.ts +183 -0
  72. package/src/index.ts +2 -1
@@ -0,0 +1,126 @@
1
+ import { html, LitElement } from 'lit';
2
+ import { customElement, property, state } from 'lit/decorators.js';
3
+ import { ifDefined } from 'lit/directives/if-defined.js';
4
+ import { createRef, ref } from 'lit/directives/ref.js';
5
+ import { styleMap } from 'lit/directives/style-map.js';
6
+
7
+ import type { DialogEmbedProps } from '.';
8
+ import type { MediaPlayer } from '@vouchfor/media-player';
9
+
10
+ import './DialogOverlay';
11
+
12
+ @customElement('vouch-embed-dialog-portal')
13
+ class DialogPortal extends LitElement {
14
+ @property({ type: String }) vouchId: DialogEmbedProps['vouchId'];
15
+ @property({ type: String }) templateId: DialogEmbedProps['templateId'];
16
+ @property({ type: Array }) questions: DialogEmbedProps['questions'];
17
+
18
+ @property({ type: String }) env: DialogEmbedProps['env'] = 'prod';
19
+ @property({ type: String }) apiKey: DialogEmbedProps['apiKey'] = '';
20
+ @property({ type: Boolean }) disableTracking: DialogEmbedProps['disableTracking'] = false;
21
+ @property({ type: String }) trackingSource: DialogEmbedProps['trackingSource'] = 'embedded_player';
22
+
23
+ @property({ type: Array }) controls: DialogEmbedProps['controls'];
24
+ @property({ type: String }) preload: DialogEmbedProps['preload'] = 'none';
25
+ @property({ type: Boolean }) disableAutoplay: DialogEmbedProps['disableAutoplay'] = false;
26
+ @property({ type: Number }) aspectRatio: DialogEmbedProps['aspectRatio'] = 0;
27
+
28
+ private _mediaPlayerRef = createRef<MediaPlayer>();
29
+
30
+ @state() open = false;
31
+
32
+ private _handleToggle = ({ detail }: CustomEvent<string>) => {
33
+ // Because we have to attach this listener to the document since this element is portalled outside of the button,
34
+ // we also have to make sure that this player is actually the one we want to open and play by passing in an ID
35
+ // from the button wrapper parent and checking against that same ID we pass as the event detail
36
+ if (this.id === detail) {
37
+ this.open = !this.open;
38
+
39
+ if (this.open) {
40
+ if (!this.disableAutoplay && this._mediaPlayerRef?.value) {
41
+ this._mediaPlayerRef.value.muted = false;
42
+ this._mediaPlayerRef.value.play();
43
+ }
44
+ } else {
45
+ this._mediaPlayerRef?.value?.pause();
46
+ }
47
+ }
48
+ };
49
+
50
+ // We could do the same thing on close and check for the correct ID but it doesn't really matter
51
+ private _handleClose = () => {
52
+ this.open = false;
53
+ this._mediaPlayerRef?.value?.pause();
54
+ };
55
+
56
+ private _handleDocumentKeyUp = (e: KeyboardEvent) => {
57
+ if (e.key === 'Escape') {
58
+ this._handleClose();
59
+ }
60
+ };
61
+
62
+ connectedCallback(): void {
63
+ super.connectedCallback();
64
+ document.addEventListener('dialogembed:click', this._handleToggle);
65
+ document.addEventListener('keyup', this._handleDocumentKeyUp);
66
+ document.addEventListener('close:click', this._handleClose);
67
+ document.addEventListener('overlay:click', this._handleClose);
68
+ }
69
+
70
+ disconnectedCallback(): void {
71
+ super.disconnectedCallback();
72
+ document.removeEventListener('dialogembed:click', this._handleToggle);
73
+ document.removeEventListener('keyup', this._handleDocumentKeyUp);
74
+ document.removeEventListener('close:click', this._handleClose);
75
+ document.removeEventListener('overlay:click', this._handleClose);
76
+ }
77
+
78
+ protected createRenderRoot(): HTMLElement | DocumentFragment {
79
+ // We must create a new node here because portalling into the same node (document.body) causes the second
80
+ // element to overwrite the first for some reason (not behaviour really stated in the docs)
81
+ // I am fairly certain this function is only run once as the default behaviour creates the open shadow root
82
+ // and returns that shadow root in this function: https://lit.dev/docs/components/shadow-dom/#implementing-createrenderroot
83
+ const newNode = document.createElement('div');
84
+ document.body.appendChild(newNode);
85
+ return newNode;
86
+ }
87
+
88
+ render() {
89
+ return html`
90
+ <vouch-embed-dialog-overlay ?open=${this.open} aspectRatio=${this.aspectRatio}>
91
+ <vouch-embed-player
92
+ ${ref(this._mediaPlayerRef)}
93
+ style=${styleMap({
94
+ maxWidth: '100%',
95
+ maxHeight: '100%'
96
+ })}
97
+ ?autoplay=${false}
98
+ vouchId=${ifDefined(this.vouchId)}
99
+ templateId=${ifDefined(this.templateId)}
100
+ .questions=${this.questions}
101
+ .controls=${this.controls}
102
+ env=${ifDefined(this.env)}
103
+ apiKey=${ifDefined(this.apiKey)}
104
+ ?disableTracking=${this.disableTracking}
105
+ trackingSource=${ifDefined(this.trackingSource)}
106
+ preload=${ifDefined(this.preload)}
107
+ aspectRatio=${this.aspectRatio}
108
+ ></vouch-embed-player>
109
+ </vouch-embed-dialog-overlay>
110
+ `;
111
+ }
112
+ }
113
+
114
+ declare global {
115
+ interface HTMLElementTagNameMap {
116
+ 'vouch-embed-dialog-portal': DialogPortal;
117
+ }
118
+
119
+ namespace JSX {
120
+ interface IntrinsicElements {
121
+ 'vouch-embed-dialog-portal': DialogPortal;
122
+ }
123
+ }
124
+ }
125
+
126
+ export { DialogPortal };
@@ -0,0 +1,97 @@
1
+ import { css, html, LitElement } from 'lit';
2
+ import { customElement, property } from 'lit/decorators.js';
3
+ import { ifDefined } from 'lit/directives/if-defined.js';
4
+ import { v4 as uuidv4 } from 'uuid';
5
+
6
+ import type { PlayerEmbedProps } from '../PlayerEmbed';
7
+
8
+ import '../PlayerEmbed';
9
+ import './DialogPortal';
10
+
11
+ type DialogEmbedProps = Omit<PlayerEmbedProps, 'data'> & {
12
+ disableAutoplay?: boolean;
13
+ };
14
+
15
+ @customElement('vouch-embed-dialog')
16
+ class DialogEmbed extends LitElement {
17
+ static styles = [
18
+ css`
19
+ :host {
20
+ --vu-button-padding: 10px 20px;
21
+ --vu-button-background: #287179;
22
+ --vu-button-background-hover: #4faab2;
23
+
24
+ display: flex;
25
+ width: fit-content;
26
+ height: fit-content;
27
+ }
28
+ `
29
+ ];
30
+
31
+ @property({ type: String }) vouchId: DialogEmbedProps['vouchId'];
32
+ @property({ type: String }) templateId: DialogEmbedProps['templateId'];
33
+ @property({ type: Array }) questions: DialogEmbedProps['questions'];
34
+
35
+ @property({ type: String }) env: DialogEmbedProps['env'] = 'prod';
36
+ @property({ type: String }) apiKey: DialogEmbedProps['apiKey'] = '';
37
+ @property({ type: Boolean }) disableTracking: DialogEmbedProps['disableTracking'] = false;
38
+ @property({ type: String }) trackingSource: DialogEmbedProps['trackingSource'] = 'embedded_player';
39
+
40
+ @property({ type: Array }) controls: DialogEmbedProps['controls'];
41
+ @property({ type: String }) preload: DialogEmbedProps['preload'] = 'none';
42
+ @property({ type: Boolean }) disableAutoplay: DialogEmbedProps['disableAutoplay'] = false;
43
+ @property({ type: Number }) aspectRatio: DialogEmbedProps['aspectRatio'] = 0;
44
+
45
+ private _id = uuidv4();
46
+
47
+ private _handleRootClick = () => {
48
+ this.dispatchEvent(new CustomEvent('dialogembed:click', { detail: this._id, bubbles: true, composed: true }));
49
+ };
50
+
51
+ connectedCallback(): void {
52
+ super.connectedCallback();
53
+ this.addEventListener('click', this._handleRootClick);
54
+ }
55
+
56
+ disconnectedCallback(): void {
57
+ super.disconnectedCallback();
58
+ this.removeEventListener('click', this._handleRootClick);
59
+ }
60
+
61
+ render() {
62
+ return html`
63
+ <slot>
64
+ <vmp-button size="large">Play</vmp-button>
65
+ </slot>
66
+ <vouch-embed-dialog-portal
67
+ id=${this._id}
68
+ ?autoplay=${false}
69
+ vouchId=${ifDefined(this.vouchId)}
70
+ templateId=${ifDefined(this.templateId)}
71
+ .questions=${this.questions}
72
+ .controls=${this.controls}
73
+ env=${ifDefined(this.env)}
74
+ apiKey=${ifDefined(this.apiKey)}
75
+ ?disableTracking=${this.disableTracking}
76
+ trackingSource=${ifDefined(this.trackingSource)}
77
+ preload=${ifDefined(this.preload)}
78
+ aspectRatio=${this.aspectRatio}
79
+ ></vouch-embed-dialog-portal>
80
+ `;
81
+ }
82
+ }
83
+
84
+ declare global {
85
+ interface HTMLElementTagNameMap {
86
+ 'vouch-embed-dialog': DialogEmbed;
87
+ }
88
+
89
+ namespace JSX {
90
+ interface IntrinsicElements {
91
+ 'vouch-embed-dialog': DialogEmbed;
92
+ }
93
+ }
94
+ }
95
+
96
+ export { DialogEmbed };
97
+ export type { DialogEmbedProps };
@@ -1,16 +1,16 @@
1
1
  import { html } from 'lit';
2
2
  import { ifDefined } from 'lit/directives/if-defined.js';
3
3
 
4
- import type { EmbedProps } from './';
4
+ import type { PlayerEmbedProps } from '.';
5
5
  import type { Meta, StoryObj } from '@storybook/web-components';
6
6
 
7
- import './';
7
+ import '.';
8
8
 
9
- type EmbedArgs = EmbedProps & {
9
+ type PlayerEmbedArgs = PlayerEmbedProps & {
10
10
  showVouch?: boolean;
11
11
  };
12
12
 
13
- const _Embed = ({
13
+ const _PlayerEmbed = ({
14
14
  vouchId,
15
15
  templateId,
16
16
  questions,
@@ -20,10 +20,10 @@ const _Embed = ({
20
20
  apiKey,
21
21
  controls,
22
22
  aspectRatio
23
- }: EmbedArgs) => {
23
+ }: PlayerEmbedArgs) => {
24
24
  return html`
25
25
  <div style="height: 100vh">
26
- <vouch-embed
26
+ <vouch-embed-player
27
27
  env=${ifDefined(env)}
28
28
  apiKey=${ifDefined(apiKey)}
29
29
  vouchId=${ifDefined(vouchId)}
@@ -34,24 +34,24 @@ const _Embed = ({
34
34
  preload=${ifDefined(preload)}
35
35
  aspectRatio=${ifDefined(aspectRatio)}
36
36
  @error=${console.log}
37
- ></vouch-embed>
37
+ ></vouch-embed-player>
38
38
  </div>
39
39
  `;
40
40
  };
41
41
 
42
42
  // More on how to set up stories at: https://storybook.js.org/docs/web-components/writing-stories/introduction
43
43
  const meta = {
44
- title: 'Embed',
44
+ title: 'Embeds',
45
45
  tags: ['autodocs'],
46
- render: (args) => _Embed(args),
47
- component: 'vouch-embed'
48
- } satisfies Meta<EmbedProps>;
46
+ render: (args) => _PlayerEmbed(args),
47
+ component: 'vouch-embed-player'
48
+ } satisfies Meta<PlayerEmbedProps>;
49
49
 
50
- type Story = StoryObj<EmbedArgs>;
50
+ type Story = StoryObj<PlayerEmbedArgs>;
51
51
 
52
- const Embed: Story = {
52
+ const Player: Story = {
53
53
  args: {
54
- env: 'local',
54
+ env: 'dev',
55
55
  apiKey: 'TVik9uTMgE-PD25UTHIS6gyl0hMBWC7AT4dkpdlLBT4VIfDWZJrQiCk6Ak7m1',
56
56
  vouchId: '6JQEIPeStt',
57
57
  templateId: '357fc118-e179-4171-9446-ff2b8e9d1b29',
@@ -76,4 +76,4 @@ const Embed: Story = {
76
76
  };
77
77
 
78
78
  export default meta;
79
- export { Embed };
79
+ export { Player };
@@ -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,29 +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
12
  type FetchTaskDeps = [
13
- EmbedProps['env'],
14
- EmbedProps['apiKey'],
15
- EmbedProps['data'],
16
- EmbedProps['vouchId'],
17
- EmbedProps['templateId']
13
+ PlayerEmbedProps['env'],
14
+ PlayerEmbedProps['apiKey'],
15
+ PlayerEmbedProps['data'],
16
+ PlayerEmbedProps['vouchId'],
17
+ PlayerEmbedProps['templateId']
18
18
  ];
19
19
 
20
- type FilterTaskDeps = [EmbedProps['data'], EmbedProps['questions']];
20
+ type FilterTaskDeps = [PlayerEmbedProps['data'], PlayerEmbedProps['questions']];
21
21
 
22
22
  class FetcherController {
23
- host: EmbedHost;
23
+ host: PlayerEmbedHost;
24
24
 
25
25
  private _fetching = false;
26
- private _vouch: EmbedProps['data'];
26
+ private _vouch: PlayerEmbedProps['data'];
27
27
 
28
28
  set fetching(value) {
29
29
  if (this._fetching !== value) {
@@ -97,7 +97,7 @@ class FetcherController {
97
97
  return template;
98
98
  };
99
99
 
100
- constructor(host: EmbedHost) {
100
+ constructor(host: PlayerEmbedHost) {
101
101
  this.host = host;
102
102
  new Task<FetchTaskDeps, void>(
103
103
  this.host,
@@ -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 };