@playpilot/tpi 5.4.0 → 5.5.0

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/events.md CHANGED
@@ -60,3 +60,9 @@ Event | Action | Info | Payload
60
60
  `ali_manual_report` | _Fires only through manual action when reporting issues with an injection via the Editor._ | | `Title`, `report_reason`, `sid` (of injection), `title` (of injection), `sentence`, `failed` (true or false), `failed_message` (reason for failure as given in the editor), `manual` (true or false)
61
61
  `ali_editor_error` | _Fires whenever an error occurs within the Editor._ | | `Title`, `phrase`, `sentence`
62
62
  `ali_injection_error` | _Fires whenever an error occurs during injection_ | This includes fetching the injections as well as actually injecting itself. Does not include fetching of the config object. | `message` (error message as given by the browser)
63
+
64
+ ### Split Testing
65
+ Event | Action | Info | Payload
66
+ --- | --- | --- | ---
67
+ `ali_split_test_view` | _Should be fired for any active split test_ | | `key`, `variant` (0 or 1)
68
+ `ali_split_test_action` | _Should be fired for any assertion in split tests_ | | `key` (matches the key of `ali_split_test_view`), `variant` (0 or 1), `action`
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@playpilot/tpi",
3
- "version": "5.4.0",
3
+ "version": "5.5.0",
4
4
  "type": "module",
5
5
  "scripts": {
6
6
  "dev": "vite dev",
@@ -86,6 +86,11 @@ export const translations = {
86
86
  [Language.Swedish]: 'Hyra',
87
87
  [Language.Danish]: 'Lej eller køb',
88
88
  },
89
+ 'Watch': {
90
+ [Language.English]: 'Watch',
91
+ [Language.Swedish]: 'Se',
92
+ [Language.Danish]: 'Se',
93
+ },
89
94
 
90
95
  // Genres
91
96
  'All': {
@@ -25,4 +25,7 @@ export const TrackingEvent = Object.freeze({
25
25
  ManualReport: 'ali_manual_report',
26
26
  EditorError: 'ali_editor_error',
27
27
  InjectionError: 'ali_injection_error',
28
+
29
+ SplitTestView: 'ali_split_test_view',
30
+ SplitTestAction: 'ali_split_test_action'
28
31
  })
@@ -0,0 +1,40 @@
1
+ import { TrackingEvent } from "./enums/TrackingEvent"
2
+ import { track } from "./tracking"
3
+
4
+ /**
5
+ * Each split test uses a different split test identifier so a user can be part of one, multiple, or none, tests.
6
+ * The identifier is saved on the window object so that it is consistent across this session.
7
+ */
8
+ export function getSplitTestIdentifier(key: string): number {
9
+ const windowIdentifier = window.PlayPilotLinkInjections?.split_test_identifiers?.[key]
10
+ if (windowIdentifier) return windowIdentifier
11
+
12
+ if (!window.PlayPilotLinkInjections?.split_test_identifiers) window.PlayPilotLinkInjections.split_test_identifiers = {}
13
+
14
+ const randomIdentifier = Math.random()
15
+ window.PlayPilotLinkInjections.split_test_identifiers[key] = randomIdentifier
16
+
17
+ return randomIdentifier
18
+ }
19
+
20
+ /**
21
+ * Split tests are either on or off with a 50/50 split. That's all we need for now.
22
+ */
23
+ export function isInSplitTest(key: string): boolean {
24
+ const identifier = getSplitTestIdentifier(key)
25
+ const isInSplitTest = identifier > 0.5
26
+
27
+ return isInSplitTest
28
+ }
29
+
30
+ export function trackSplitTestView(key: string): void {
31
+ const variant = isInSplitTest(key) ? 1 : 0
32
+
33
+ track(TrackingEvent.SplitTestView, null, { key, variant })
34
+ }
35
+
36
+ export function trackSplitTestAction(key: string, action: string): void {
37
+ const variant = isInSplitTest(key) ? 1 : 0
38
+
39
+ track(TrackingEvent.SplitTestAction, null, { key, variant, action })
40
+ }
@@ -10,7 +10,7 @@ const baseUrl = 'https://insights.playpilot.net'
10
10
  * @param [title] Title related to the event
11
11
  * @param [payload] Any data that will be included with the event
12
12
  */
13
- export async function track(event: string, title: TitleData | null = null, payload: Record<string, string | string[] | number | null | undefined> = {}): Promise<void> {
13
+ export async function track(event: string, title: TitleData | null = null, payload: Record<string, string | string[] | number | boolean | null | undefined> = {}): Promise<void> {
14
14
  const headers = new Headers({ 'Content-Type': 'application/json' })
15
15
 
16
16
  if (title) {
@@ -9,4 +9,5 @@ export type ScriptConfig = {
9
9
  language?: string | null
10
10
  last_successful_fetch?: LinkInjectionResponse | null
11
11
  tracked_events?: { event: string, payload: Record<string, any> }[]
12
+ split_test_identifiers?: Record<string, number>
12
13
  }
package/src/main.ts CHANGED
@@ -14,6 +14,7 @@ window.PlayPilotLinkInjections = {
14
14
  domain_sid: null,
15
15
  last_successful_fetch: null,
16
16
  tracked_events: [],
17
+ split_test_identifiers: {},
17
18
  app: null,
18
19
 
19
20
  initialize(config = { token: '', selector: '', after_article_selector: '', after_article_insert_position: '', language: null, organization_sid: null, domain_sid: null, editorial_token: '' }): void {
@@ -81,6 +82,10 @@ window.PlayPilotLinkInjections = {
81
82
  console.groupCollapsed('Tracked events')
82
83
  console.log(this.tracked_events)
83
84
  console.groupEnd()
85
+
86
+ console.groupCollapsed('Split tests')
87
+ console.log(this.split_test_identifiers)
88
+ console.groupEnd()
84
89
  console.groupEnd()
85
90
  }
86
91
  }
@@ -7,9 +7,10 @@
7
7
  import { isCrawler } from '$lib/crawler'
8
8
  import { TrackingEvent } from '$lib/enums/TrackingEvent'
9
9
  import { authorize, getAuthToken, isEditorialModeEnabled, removeAuthCookie, setEditorialParamInUrl } from '$lib/auth'
10
+ import { trackSplitTestView } from '$lib/splitTest'
11
+ import type { LinkInjectionResponse, LinkInjection, LinkInjectionTypes } from '$lib/types/injection'
10
12
  import Editor from './components/Editorial/Editor.svelte'
11
13
  import EditorTrigger from './components/Editorial/EditorTrigger.svelte'
12
- import type { LinkInjectionResponse, LinkInjection, LinkInjectionTypes } from '$lib/types/injection'
13
14
  import Alert from './components/Editorial/Alert.svelte'
14
15
  import TrackingPixels from './components/TrackingPixels.svelte'
15
16
 
@@ -40,6 +41,8 @@
40
41
  (async () => {
41
42
  await initialize()
42
43
  track(TrackingEvent.ArticlePageView)
44
+
45
+ trackSplitTestView('initial_test')
43
46
  })()
44
47
 
45
48
  return () => clearLinkInjections()
@@ -6,20 +6,22 @@
6
6
  import type { PlaylinkData } from '$lib/types/playlink'
7
7
  import type { TitleData } from '$lib/types/title'
8
8
  import { heading } from '$lib/actions/heading'
9
- import IconContinue from './Icons/IconContinue.svelte'
10
9
  import { getContext } from 'svelte'
11
10
  import { removeImageUrlPrefix } from '$lib/image'
12
11
 
13
12
  interface Props {
14
13
  playlinks: PlaylinkData[]
15
14
  title: TitleData
16
- list?: boolean
17
15
  }
18
16
 
19
- const { playlinks, title, list = false }: Props = $props()
17
+ const { playlinks, title }: Props = $props()
20
18
 
21
19
  const isModal = getContext('scope') === 'modal'
22
20
 
21
+ let outerWidth = $state(0)
22
+
23
+ const list = $derived(outerWidth < 500)
24
+
23
25
  // Remove any playlinks without logos, as these are likely sub providers.
24
26
  const filteredPlaylinks = $derived(playlinks.filter(playlink => !!playlink.logo_url))
25
27
  const mergedPlaylink = $derived(mergePlaylinks(filteredPlaylinks))
@@ -45,21 +47,23 @@
45
47
  </div>
46
48
  {/if}
47
49
 
48
- <div class="playlinks" class:list>
50
+ <div class="playlinks" class:list bind:clientWidth={outerWidth}>
49
51
  {#each mergedPlaylink as { name, url, logo_url, highlighted, cta_text, extra_info: { category } }}
50
52
  <a href={url} target="_blank" class="playlink" class:highlighted={highlighted && cta_text} onclick={() => onclick(name)} data-playlink={name} rel="sponsored">
51
- <img src={removeImageUrlPrefix(logo_url)} alt="" height="32" width="32" />
53
+ <div class="playlink-content">
54
+ <img src={removeImageUrlPrefix(logo_url)} alt="" height="36" width="36" />
52
55
 
53
- <div>
54
56
  <span class="name">{name}</span>
55
- <div class="category">{cta_text || categoryStrings[category] || t('Stream')}</div>
56
- </div>
57
+ <span class="category">{categoryStrings[category] || t('Stream')}</span>
57
58
 
58
- {#if list}
59
- <div class="arrow">
60
- <IconContinue />
59
+ {#if cta_text}
60
+ <span class="cta">{cta_text}</span>
61
+ {/if}
62
+
63
+ <div class="action">
64
+ {t('Watch')}
61
65
  </div>
62
- {/if}
66
+ </div>
63
67
  </a>
64
68
  {/each}
65
69
 
@@ -71,16 +75,13 @@
71
75
  </div>
72
76
 
73
77
  <style lang="scss">
78
+ $image-size: margin(2.25);
79
+
74
80
  img {
75
- --size: #{margin(2)};
76
- height: var(--size);
77
- width: var(--size);
81
+ height: $image-size;
82
+ width: $image-size;
78
83
  border-radius: margin(0.5);
79
84
  background: rgba(0, 0, 0, 0.25);
80
-
81
- @media (min-width: 640px) {
82
- --size: #{margin(2.5)};
83
- }
84
85
  }
85
86
 
86
87
  .heading {
@@ -94,40 +95,35 @@
94
95
 
95
96
  .playlinks {
96
97
  box-sizing: border-box;
97
- display: flex;
98
+ display: grid;
99
+ grid-template-columns: repeat(auto-fill, minmax(margin(15), 1fr));
98
100
  gap: margin(0.5);
99
101
  width: calc(100% + margin(2));
100
102
  padding: margin(0.5) margin(1);
101
103
  margin: 0 margin(-1);
102
- overflow: auto;
103
- scrollbar-width: none;
104
- }
105
104
 
106
- .playlinks::-webkit-scrollbar {
107
- display: none;
105
+ &.list {
106
+ grid-template-columns: 1fr;
107
+ }
108
108
  }
109
109
 
110
- .list {
111
- display: grid;
112
- grid-template-columns: repeat(auto-fill, minmax(margin(15), 1fr));
113
110
 
114
- img {
115
- @media (min-width: 640px) {
116
- --size: #{margin(2)};
117
- }
111
+ @keyframes sheen {
112
+ 0%, 20%, 60%, 80.01%, 100% {
113
+ background-position: -100% 0;
114
+ }
115
+
116
+ 80% {
117
+ background-position: 100% 0;
118
118
  }
119
119
  }
120
120
 
121
121
  .playlink {
122
122
  position: relative;
123
- display: flex;
124
- align-items: center;
125
- gap: margin(0.75);
126
- padding: margin(0.75);
127
123
  background: var(--playpilot-playlink-background, var(--playpilot-lighter));
128
124
  box-shadow: var(--playpilot-playlink-shadow, var(--playpilot-shadow));
129
125
  border-radius: var(--playpilot-playlink-border-radius, margin(0.5));
130
- color: var(--playpilot-playlink-text-color, var(--playpilot-text-color-alt)) !important;
126
+ color: var(--playpilot-playlink-text-color, var(--playpilot-text-color)) !important;
131
127
  font-weight: var(--playpilot-playlink-font-weight, inherit);
132
128
  font-style: var(--playpilot-playlink-font-style, normal) !important;
133
129
  text-decoration: none !important;
@@ -143,6 +139,12 @@
143
139
  }
144
140
 
145
141
  &.highlighted {
142
+ grid-column: span 2;
143
+
144
+ .list & {
145
+ grid-column: 1;
146
+ }
147
+
146
148
  &::before {
147
149
  content: "";
148
150
  z-index: 1;
@@ -153,35 +155,81 @@
153
155
  bottom: 0;
154
156
  left: 0;
155
157
  border-radius: inherit;
156
- box-shadow: inset 0 0 0 2px currentColor;
158
+ background: linear-gradient(to left, currentColor 20%, transparent 50%, currentColor 80%);
159
+ background-size: 200% 100%;
160
+ background-position: -100% 0;
157
161
  opacity: 0.35;
158
162
  pointer-events: none;
163
+
164
+ @media (prefers-reduced-motion: no-preference) {
165
+ animation: sheen 4000ms ease-in-out infinite;
166
+ }
167
+ }
168
+
169
+ &::after {
170
+ content: "";
171
+ z-index: 2;
172
+ display: block;
173
+ position: absolute;
174
+ top: 2px;
175
+ right: 2px;
176
+ bottom: 2px;
177
+ left: 2px;
178
+ border-radius: inherit;
179
+ background: var(--playpilot-playlink-background, var(--playpilot-lighter));
159
180
  }
160
181
  }
161
182
 
162
183
  img {
184
+ grid-area: image;
163
185
  margin: 0;
164
186
  }
187
+ }
188
+
189
+ .playlink-content {
190
+ position: relative;
191
+ z-index: 5;
192
+ display: grid;
193
+ grid-template-areas: "image name action" "image category action";
194
+ grid-template-columns: $image-size auto margin(4);
195
+ align-items: center;
196
+ gap: 0 margin(0.75);
197
+ padding: margin(0.75);
165
198
 
166
- .list & {
167
- padding: margin(0.5);
168
- gap: margin(0.5);
199
+ .highlighted & {
200
+ grid-template-areas: "image name action" "image category action" "image cta action";
201
+ gap: margin(0.25) margin(0.75);
169
202
  }
170
203
  }
171
204
 
172
205
  .name {
173
- font-weight: var(--playpilot-playlink-font-weight, inherit);
206
+ grid-area: name;
207
+ width: 100%;
208
+ overflow: hidden;
209
+ font-weight: var(--playpilot-playlink-font-weight, 500);
174
210
  font-family: var(--playpilot-playlink-font-family, inherit);
211
+ text-overflow: ellipsis;
212
+ white-space: nowrap;
175
213
  }
176
214
 
177
215
  .category {
178
- margin-top: var(--playpilot-playlinks-category-margin, margin(0.5));
216
+ grid-area: category;
217
+ width: 100%;
179
218
  font-size: var(--playpilot-playlinks-category-font-size, margin(0.625));
180
- color: var(--playpilot-playlink-category-text-color, var(--playpilot-text-color));
219
+ color: var(--playpilot-playlink-category-text-color, var(--playpilot-text-color-alt));
181
220
  font-weight: var(--playpilot-playlink-category-font-weight, inherit);
182
221
  font-family: var(--playpilot-playlink-category-font-family, inherit);
183
222
  }
184
223
 
224
+ .cta {
225
+ grid-area: cta;
226
+ width: 100%;
227
+ font-size: var(--playpilot-playlinks-cta-font-size, var(--playpilot-playlinks-category-font-size, margin(0.625)));
228
+ color: var(--playpilot-playlink-cta-text-color, var(--playpilot-playlink-category-text-color, var(--playpilot-text-color)));
229
+ font-weight: var(--playpilot-playlink-cta-font-weight, var(--playpilot-playlink-category-font-weight, inherit));
230
+ font-family: var(--playpilot-playlink-cta-font-family, var(--playpilot-playlink-category-font-family, inherit));
231
+ }
232
+
185
233
  .disclaimer {
186
234
  margin-top: var(--playpilot-playlinks-disclaimer-margin-top, margin(0.5));
187
235
  opacity: var(--playpilot-playlinks-disclaimer-opacity, 0.65);
@@ -205,9 +253,13 @@
205
253
  line-height: 1.35;
206
254
  }
207
255
 
208
- .arrow {
209
- display: flex;
256
+ .action {
257
+ grid-area: action;
210
258
  margin-left: auto;
211
- padding: 0 margin(0.5);
259
+ padding: margin(0.5);
260
+ font-weight: var(--playpilot-playlinks-action-font-weight, 500);
261
+ color: var(--playpilot-playlinks-action-text-color, var(--playpilot-text-color));
262
+ border: var(--playpilot-playlinks-action-border, 1px solid currentColor);
263
+ border-radius: var(--playpilot-playlinks-action-border-radius, margin(2));
212
264
  }
213
265
  </style>
@@ -91,7 +91,7 @@
91
91
  border-radius: var(--playpilot-popover-border-radius, margin(1));
92
92
  background: var(--playpilot-popover-background, var(--playpilot-light));
93
93
  box-shadow: var(--playpilot-popover-shadow, var(--playpilot-shadow-large));
94
- max-height: min(var(--max-height), margin(35));
94
+ max-height: min(var(--max-height), margin(36));
95
95
  scrollbar-width: thin;
96
96
  overflow-y: overlay;
97
97
  overflow-x: hidden;
@@ -54,7 +54,7 @@
54
54
  </div>
55
55
 
56
56
  <div class="main">
57
- <Playlinks playlinks={title.providers} {title} list />
57
+ <Playlinks playlinks={title.providers} {title} />
58
58
 
59
59
  {#if !small}
60
60
  <Description text={title.description} blurb={title.blurb} />
@@ -2,6 +2,7 @@
2
2
  import { TrackingEvent } from '$lib/enums/TrackingEvent'
3
3
  import { track } from '$lib/tracking'
4
4
  import type { TitleData } from '$lib/types/title'
5
+ import { trackSplitTestAction } from '$lib/splitTest'
5
6
  import { onMount } from 'svelte'
6
7
  import Modal from './Modal.svelte'
7
8
  import Title from './Title.svelte'
@@ -19,6 +20,9 @@
19
20
 
20
21
  onMount(() => {
21
22
  const openTimestamp = Date.now()
23
+
24
+ trackSplitTestAction('initial_test', 'modal')
25
+
22
26
  return () => track(TrackingEvent.TitleModalClose, title, { time_spent: Date.now() - openTimestamp })
23
27
  })
24
28
 
@@ -0,0 +1,88 @@
1
+ import { describe, it, expect, vi, afterEach } from 'vitest'
2
+ import { getSplitTestIdentifier, isInSplitTest, trackSplitTestAction, trackSplitTestView } from '$lib/splitTest'
3
+ import { track } from '$lib/tracking'
4
+ import { TrackingEvent } from '$lib/enums/TrackingEvent'
5
+
6
+ vi.mock('$lib/tracking', () => ({
7
+ track: vi.fn(),
8
+ }))
9
+
10
+ describe('$lib/splitTest', () => {
11
+ describe('getSplitTestIdentifier', () => {
12
+ afterEach(() => {
13
+ // @ts-ignore
14
+ window.PlayPilotLinkInjections = {}
15
+
16
+ vi.resetAllMocks()
17
+ })
18
+
19
+ it('Should set window object with given key if window object was not set to begin with', () => {
20
+ // @ts-ignore
21
+ window.PlayPilotLinkInjections = {}
22
+
23
+ const value = getSplitTestIdentifier('Some key')
24
+
25
+ expect(window.PlayPilotLinkInjections.split_test_identifiers).toEqual({ 'Some key': value })
26
+ // @ts-ignore
27
+ expect(value).toBe(window.PlayPilotLinkInjections.split_test_identifiers['Some key'])
28
+ })
29
+
30
+ it('Should return value as stored in the window object', () => {
31
+ // @ts-ignore
32
+ window.PlayPilotLinkInjections.split_test_identifiers = {}
33
+ window.PlayPilotLinkInjections.split_test_identifiers['Some key'] = 0.5
34
+
35
+ expect(getSplitTestIdentifier('Some key')).toBe(0.5)
36
+ })
37
+
38
+ it('Should set separate value for different keys', () => {
39
+ // @ts-ignore
40
+ window.PlayPilotLinkInjections.split_test_identifiers = {}
41
+
42
+ getSplitTestIdentifier('Some key')
43
+ getSplitTestIdentifier('Some other key')
44
+
45
+ expect(window.PlayPilotLinkInjections.split_test_identifiers).toEqual({
46
+ 'Some key': expect.any(Number),
47
+ 'Some other key': expect.any(Number),
48
+ })
49
+ })
50
+ })
51
+
52
+ describe('isInSplitTest', () => {
53
+ it('Should return true if stored value is higher than 0.5', () => {
54
+ // @ts-ignore
55
+ window.PlayPilotLinkInjections.split_test_identifiers = {}
56
+ window.PlayPilotLinkInjections.split_test_identifiers['Some key'] = 0.75
57
+
58
+ expect(isInSplitTest('Some key')).toBe(true)
59
+ })
60
+
61
+ it('Should return false if stored value is lower than 0.5', () => {
62
+ // @ts-ignore
63
+ window.PlayPilotLinkInjections.split_test_identifiers = {}
64
+ window.PlayPilotLinkInjections.split_test_identifiers['Some key'] = 0.25
65
+
66
+ expect(isInSplitTest('Some key')).toBe(false)
67
+ })
68
+ })
69
+
70
+ describe('trackSplitTestView', () => {
71
+ it('Should track view event with the given key', () => {
72
+ trackSplitTestView('Some key')
73
+
74
+ const variant = isInSplitTest('Some key') ? 1 : 0
75
+
76
+ expect(track).toHaveBeenCalledWith(TrackingEvent.SplitTestView, null, { key: 'Some key', variant })
77
+ })
78
+ })
79
+
80
+ describe('trackSplitTestAction', () => {
81
+ it('Should track action event with the given key and action', () => {
82
+ trackSplitTestAction('Some key', 'Some action')
83
+ const variant = isInSplitTest('Some key') ? 1 : 0
84
+
85
+ expect(track).toHaveBeenCalledWith(TrackingEvent.SplitTestAction, null, { key: 'Some key', variant, action: 'Some action' })
86
+ })
87
+ })
88
+ })
@@ -57,6 +57,10 @@ vi.mock('$lib/url', () => ({
57
57
  getFullUrlPath: vi.fn(),
58
58
  }))
59
59
 
60
+ vi.mock('$lib/splitTest', () => ({
61
+ trackSplitTestView: vi.fn(),
62
+ }))
63
+
60
64
  describe('$routes/+page.svelte', () => {
61
65
  beforeEach(() => {
62
66
  document.body.innerHTML = ''
@@ -104,19 +104,7 @@ describe('Playlinks.svelte', () => {
104
104
  expect(getAllByText('Some playlink')).toHaveLength(1)
105
105
  })
106
106
 
107
- it('Should not have list class by default', () => {
108
- const { container } = render(Playlinks, { playlinks: [], title })
109
-
110
- expect(container.querySelector('.list')).not.toBeTruthy()
111
- })
112
-
113
- it('Should have list class when prop is given', () => {
114
- const { container } = render(Playlinks, { playlinks: [], title, list: true })
115
-
116
- expect(container.querySelector('.list')).toBeTruthy()
117
- })
118
-
119
- it('Should highlight and replace category text for playlinks that have highlighted as true', () => {
107
+ it('Should highlight and show category text for playlinks that have highlighted as true', () => {
120
108
  const playlinks = [
121
109
  { name: 'Some playlink', logo_url: 'logo', extra_info: { category: 'SVOD' } },
122
110
  { name: 'Some other playlink', logo_url: 'logo', highlighted: true, cta_text: '', extra_info: { category: 'BUY' } },