@vouchfor/embeds 0.0.0-experiment.df12bef → 0.0.0-experiment.e7e20dd
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/embeds.js +979 -1453
- package/dist/es/embeds.js.map +1 -1
- package/dist/es/src/components/DialogEmbed/DialogOverlay.d.ts +20 -0
- package/dist/es/src/components/DialogEmbed/DialogPortal.d.ts +36 -0
- package/dist/es/src/components/DialogEmbed/index.d.ts +38 -0
- package/dist/es/src/components/PlayerEmbed/controllers/event-forwarder.d.ts +15 -0
- package/dist/es/src/components/{Embed → PlayerEmbed}/controllers/fetcher.d.ts +5 -4
- package/dist/es/src/components/{Embed/controllers/tracking.d.ts → PlayerEmbed/controllers/tracking/index.d.ts} +14 -11
- package/dist/es/src/components/PlayerEmbed/controllers/tracking/utils.d.ts +17 -0
- package/dist/es/src/components/{Embed → PlayerEmbed}/index.d.ts +28 -21
- package/dist/es/src/components/PlayerEmbed/tests/data.d.ts +3 -0
- package/dist/es/src/index.d.ts +2 -1
- package/dist/iife/dialog-embed/embed.iife.js +1750 -0
- package/dist/iife/dialog-embed/embed.iife.js.map +1 -0
- package/dist/iife/embeds.iife.js +662 -457
- package/dist/iife/embeds.iife.js.map +1 -1
- package/dist/iife/player-embed/embed.iife.js +1612 -0
- package/dist/iife/player-embed/embed.iife.js.map +1 -0
- package/package.json +43 -31
- package/src/components/DialogEmbed/Dialog.stories.ts +91 -0
- package/src/components/DialogEmbed/DialogOverlay.ts +131 -0
- package/src/components/DialogEmbed/DialogPortal.ts +126 -0
- package/src/components/DialogEmbed/index.ts +97 -0
- package/src/components/{Embed/Embed.stories.ts → PlayerEmbed/PlayerEmbed.stories.ts} +26 -14
- package/src/components/{Embed → PlayerEmbed}/controllers/event-forwarder.ts +6 -5
- package/src/components/{Embed → PlayerEmbed}/controllers/fetcher.ts +33 -14
- package/src/components/{Embed/controllers/tracking.ts → PlayerEmbed/controllers/tracking/index.ts} +47 -115
- package/src/components/PlayerEmbed/controllers/tracking/utils.ts +95 -0
- package/src/components/{Embed → PlayerEmbed}/index.ts +71 -27
- package/src/components/PlayerEmbed/tests/PlayerEmbed.spec.ts +80 -0
- package/src/components/PlayerEmbed/tests/data.ts +183 -0
- package/src/index.ts +2 -1
| @@ -1,26 +1,29 @@ | |
| 1 1 | 
             
            import { Task } from '@lit/task';
         | 
| 2 2 | 
             
            import { v4 as uuidv4 } from 'uuid';
         | 
| 3 3 |  | 
| 4 | 
            -
            import type {  | 
| 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  | 
| 10 | 
            +
            type PlayerEmbedHost = ReactiveControllerHost & PlayerEmbed;
         | 
| 11 11 |  | 
| 12 | 
            -
            type  | 
| 13 | 
            -
               | 
| 14 | 
            -
               | 
| 15 | 
            -
               | 
| 16 | 
            -
               | 
| 17 | 
            -
               | 
| 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:  | 
| 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:  | 
| 100 | 
            +
              constructor(host: PlayerEmbedHost) {
         | 
| 98 101 | 
             
                this.host = host;
         | 
| 99 | 
            -
                new Task< | 
| 102 | 
            +
                new Task<FetchTaskDeps, void>(
         | 
| 100 103 | 
             
                  this.host,
         | 
| 101 | 
            -
                  async ([env, apiKey, data, vouchId, templateId]:  | 
| 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 | 
            -
                         | 
| 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 | 
            -
                         | 
| 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 |  | 
    
        package/src/components/{Embed/controllers/tracking.ts → PlayerEmbed/controllers/tracking/index.ts}
    RENAMED
    
    | @@ -1,15 +1,13 @@ | |
| 1 | 
            -
            import {  | 
| 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  | 
| 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  | 
| 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:  | 
| 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:  | 
| 46 | 
            +
              constructor(host: PlayerEmbedHost) {
         | 
| 45 47 | 
             
                this.host = host;
         | 
| 46 48 | 
             
                host.addController(this);
         | 
| 47 49 | 
             
              }
         | 
| 48 50 |  | 
| 49 | 
            -
              private  | 
| 50 | 
            -
                 | 
| 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 ( | 
| 98 | 
            -
                   | 
| 99 | 
            -
                  window.sessionStorage?.setItem?.('vouch-uid-tab', tabId);
         | 
| 54 | 
            +
                if (!vouchId || this.host.disableTracking) {
         | 
| 55 | 
            +
                  return;
         | 
| 100 56 | 
             
                }
         | 
| 101 57 |  | 
| 102 | 
            -
                 | 
| 103 | 
            -
                   | 
| 104 | 
            -
                   | 
| 105 | 
            -
             | 
| 106 | 
            -
             | 
| 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 = ( | 
| 135 | 
            -
                 | 
| 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. | 
| 74 | 
            +
                const { client, tab, request, visitor } = getUids(this.host.env);
         | 
| 143 75 |  | 
| 144 76 | 
             
                navigator.sendBeacon(
         | 
| 145 | 
            -
                  `${publicApiUrl}/api/ | 
| 77 | 
            +
                  `${publicApiUrl}/api/batchevents`,
         | 
| 146 78 | 
             
                  JSON.stringify({
         | 
| 147 | 
            -
                    event,
         | 
| 148 79 | 
             
                    payload: {
         | 
| 149 | 
            -
                       | 
| 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. | 
| 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. | 
| 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. | 
| 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. | 
| 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 | 
| 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. | 
| 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. | 
| 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 | 
            -
                 | 
| 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 | 
            -
                 | 
| 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 };
         | 
| @@ -1,11 +1,10 @@ | |
| 1 | 
            -
            import { html, LitElement } from 'lit';
         | 
| 1 | 
            +
            import { css, html, LitElement } from 'lit';
         | 
| 2 2 | 
             
            import { customElement, property, state } from 'lit/decorators.js';
         | 
| 3 3 | 
             
            import { ifDefined } from 'lit/directives/if-defined.js';
         | 
| 4 4 | 
             
            import { createRef, ref } from 'lit/directives/ref.js';
         | 
| 5 5 |  | 
| 6 6 | 
             
            import type { Scene, Scenes, TemplateInstance } from '@vouchfor/canvas-video';
         | 
| 7 7 | 
             
            import type { MediaPlayer, MediaPlayerProps } from '@vouchfor/media-player';
         | 
| 8 | 
            -
            import type { Ref } from 'lit/directives/ref.js';
         | 
| 9 8 | 
             
            import type { Environment } from '~/utils/env';
         | 
| 10 9 |  | 
| 11 10 | 
             
            import { EventForwardController } from './controllers/event-forwarder';
         | 
| @@ -14,32 +13,41 @@ import { TrackingController } from './controllers/tracking'; | |
| 14 13 |  | 
| 15 14 | 
             
            import '@vouchfor/media-player';
         | 
| 16 15 |  | 
| 17 | 
            -
            type  | 
| 16 | 
            +
            type PlayerEmbedProps = Pick<MediaPlayerProps, 'data' | 'aspectRatio' | 'preload' | 'autoplay' | 'controls'> & {
         | 
| 18 17 | 
             
              env: Environment;
         | 
| 19 18 | 
             
              apiKey: string;
         | 
| 20 19 | 
             
              disableTracking?: boolean;
         | 
| 21 20 | 
             
              trackingSource?: string;
         | 
| 22 21 | 
             
              vouchId?: string;
         | 
| 23 22 | 
             
              templateId?: string;
         | 
| 23 | 
            +
              // Index of the questions to include starting from 1
         | 
| 24 | 
            +
              questions?: number[];
         | 
| 24 25 | 
             
            };
         | 
| 25 26 |  | 
| 26 | 
            -
            @customElement('vouch-embed')
         | 
| 27 | 
            -
            class  | 
| 28 | 
            -
               | 
| 29 | 
            -
             | 
| 30 | 
            -
             | 
| 31 | 
            -
             | 
| 32 | 
            -
             | 
| 33 | 
            -
             | 
| 34 | 
            -
               | 
| 35 | 
            -
             | 
| 36 | 
            -
              @property({ type:  | 
| 37 | 
            -
              @property({ type: String })  | 
| 38 | 
            -
             | 
| 39 | 
            -
              @property({ type: Array })  | 
| 40 | 
            -
             | 
| 41 | 
            -
              @property({ type:  | 
| 42 | 
            -
              @property({ type:  | 
| 27 | 
            +
            @customElement('vouch-embed-player')
         | 
| 28 | 
            +
            class PlayerEmbed extends LitElement {
         | 
| 29 | 
            +
              static styles = [
         | 
| 30 | 
            +
                css`
         | 
| 31 | 
            +
                  :host {
         | 
| 32 | 
            +
                    display: flex;
         | 
| 33 | 
            +
                  }
         | 
| 34 | 
            +
                `
         | 
| 35 | 
            +
              ];
         | 
| 36 | 
            +
             | 
| 37 | 
            +
              @property({ type: Object }) data: PlayerEmbedProps['data'];
         | 
| 38 | 
            +
              @property({ type: String }) vouchId: PlayerEmbedProps['vouchId'];
         | 
| 39 | 
            +
              @property({ type: String }) templateId: PlayerEmbedProps['templateId'];
         | 
| 40 | 
            +
              @property({ type: Array }) questions: PlayerEmbedProps['questions'];
         | 
| 41 | 
            +
             | 
| 42 | 
            +
              @property({ type: String }) env: PlayerEmbedProps['env'] = 'prod';
         | 
| 43 | 
            +
              @property({ type: String }) apiKey: PlayerEmbedProps['apiKey'] = '';
         | 
| 44 | 
            +
              @property({ type: Boolean }) disableTracking: PlayerEmbedProps['disableTracking'] = false;
         | 
| 45 | 
            +
              @property({ type: String }) trackingSource: PlayerEmbedProps['trackingSource'] = 'embedded_player';
         | 
| 46 | 
            +
             | 
| 47 | 
            +
              @property({ type: Array }) controls: PlayerEmbedProps['controls'];
         | 
| 48 | 
            +
              @property({ type: String }) preload: PlayerEmbedProps['preload'] = 'auto';
         | 
| 49 | 
            +
              @property({ type: Boolean }) autoplay: PlayerEmbedProps['autoplay'] = false;
         | 
| 50 | 
            +
              @property({ type: Number }) aspectRatio: PlayerEmbedProps['aspectRatio'] = 0;
         | 
| 43 51 |  | 
| 44 52 | 
             
              private eventController = new EventForwardController(this, [
         | 
| 45 53 | 
             
                'durationchange',
         | 
| @@ -74,17 +82,23 @@ class Embed extends LitElement { | |
| 74 82 | 
             
              // @ts-ignore
         | 
| 75 83 | 
             
              private _trackingController = new TrackingController(this);
         | 
| 76 84 |  | 
| 77 | 
            -
              @state() vouch:  | 
| 85 | 
            +
              @state() vouch: PlayerEmbedProps['data'];
         | 
| 78 86 | 
             
              @state() template: TemplateInstance | undefined;
         | 
| 79 87 |  | 
| 80 88 | 
             
              get fetching() {
         | 
| 81 89 | 
             
                return this._fetcherController.fetching;
         | 
| 82 90 | 
             
              }
         | 
| 83 91 |  | 
| 92 | 
            +
              private _mediaPlayerRef = createRef<MediaPlayer>();
         | 
| 93 | 
            +
             | 
| 84 94 | 
             
              get waiting() {
         | 
| 85 95 | 
             
                return this._mediaPlayerRef.value?.waiting;
         | 
| 86 96 | 
             
              }
         | 
| 87 97 |  | 
| 98 | 
            +
              get initialised() {
         | 
| 99 | 
            +
                return this._mediaPlayerRef.value?.initialised;
         | 
| 100 | 
            +
              }
         | 
| 101 | 
            +
             | 
| 88 102 | 
             
              get seeking() {
         | 
| 89 103 | 
             
                return this._mediaPlayerRef.value?.seeking;
         | 
| 90 104 | 
             
              }
         | 
| @@ -169,13 +183,43 @@ class Embed extends LitElement { | |
| 169 183 | 
             
                this._mediaPlayerRef.value?.pause();
         | 
| 170 184 | 
             
              }
         | 
| 171 185 |  | 
| 186 | 
            +
              reset(time = 0, play = false) {
         | 
| 187 | 
            +
                this._mediaPlayerRef.value?.reset(time, play);
         | 
| 188 | 
            +
              }
         | 
| 189 | 
            +
             | 
| 172 190 | 
             
              setScene(index: number) {
         | 
| 173 191 | 
             
                this._mediaPlayerRef.value?.setScene(index);
         | 
| 174 192 | 
             
              }
         | 
| 175 193 |  | 
| 194 | 
            +
              private _renderStyles() {
         | 
| 195 | 
            +
                if (!this.aspectRatio) {
         | 
| 196 | 
            +
                  return html`
         | 
| 197 | 
            +
                    <style>
         | 
| 198 | 
            +
                      :host {
         | 
| 199 | 
            +
                        width: 100%;
         | 
| 200 | 
            +
                        height: 100%;
         | 
| 201 | 
            +
                      }
         | 
| 202 | 
            +
                    </style>
         | 
| 203 | 
            +
                  `;
         | 
| 204 | 
            +
                }
         | 
| 205 | 
            +
             | 
| 206 | 
            +
                if (typeof this.aspectRatio === 'number') {
         | 
| 207 | 
            +
                  return html`
         | 
| 208 | 
            +
                    <style>
         | 
| 209 | 
            +
                      :host {
         | 
| 210 | 
            +
                        aspect-ratio: ${this.aspectRatio};
         | 
| 211 | 
            +
                      }
         | 
| 212 | 
            +
                    </style>
         | 
| 213 | 
            +
                  `;
         | 
| 214 | 
            +
                }
         | 
| 215 | 
            +
             | 
| 216 | 
            +
                return null;
         | 
| 217 | 
            +
              }
         | 
| 218 | 
            +
             | 
| 176 219 | 
             
              render() {
         | 
| 177 220 | 
             
                return html`
         | 
| 178 | 
            -
                   | 
| 221 | 
            +
                  ${this._renderStyles()}
         | 
| 222 | 
            +
                  <vmp-media-player
         | 
| 179 223 | 
             
                    ${ref(this._mediaPlayerRef)}
         | 
| 180 224 | 
             
                    ${this.eventController.register()}
         | 
| 181 225 | 
             
                    ?autoplay=${this.autoplay}
         | 
| @@ -185,22 +229,22 @@ class Embed extends LitElement { | |
| 185 229 | 
             
                    aspectRatio=${ifDefined(this.aspectRatio)}
         | 
| 186 230 | 
             
                    preload=${ifDefined(this.preload)}
         | 
| 187 231 | 
             
                    .controls=${this.controls}
         | 
| 188 | 
            -
                  ></vmp- | 
| 232 | 
            +
                  ></vmp-media-player>
         | 
| 189 233 | 
             
                `;
         | 
| 190 234 | 
             
              }
         | 
| 191 235 | 
             
            }
         | 
| 192 236 |  | 
| 193 237 | 
             
            declare global {
         | 
| 194 238 | 
             
              interface HTMLElementTagNameMap {
         | 
| 195 | 
            -
                'vouch-embed':  | 
| 239 | 
            +
                'vouch-embed-player': PlayerEmbed;
         | 
| 196 240 | 
             
              }
         | 
| 197 241 |  | 
| 198 242 | 
             
              namespace JSX {
         | 
| 199 243 | 
             
                interface IntrinsicElements {
         | 
| 200 | 
            -
                  'vouch-embed':  | 
| 244 | 
            +
                  'vouch-embed-player': PlayerEmbed;
         | 
| 201 245 | 
             
                }
         | 
| 202 246 | 
             
              }
         | 
| 203 247 | 
             
            }
         | 
| 204 248 |  | 
| 205 | 
            -
            export {  | 
| 206 | 
            -
            export type {  | 
| 249 | 
            +
            export { PlayerEmbed };
         | 
| 250 | 
            +
            export type { PlayerEmbedProps };
         |