@playpilot/tpi 7.0.4 → 7.2.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@playpilot/tpi",
3
- "version": "7.0.4",
3
+ "version": "7.2.0",
4
4
  "type": "module",
5
5
  "scripts": {
6
6
  "dev": "vite dev",
package/src/lib/hash.ts CHANGED
@@ -13,6 +13,14 @@ export function stringToHash(string: string): string {
13
13
  return hash.toString(16)
14
14
  }
15
15
 
16
- export function generateRandomHash(): string {
17
- return (Math.random() + 1).toString(36).substring(7)
16
+ export function generateRandomHash(length = 5): string {
17
+ const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'
18
+
19
+ let hash = ''
20
+
21
+ while (hash.length < length) {
22
+ hash += characters[Math.floor(Math.random() * characters.length)]
23
+ }
24
+
25
+ return hash
18
26
  }
@@ -4,6 +4,7 @@ import { isCrawler } from './crawler'
4
4
  import { genreSlugsToNames } from './genre'
5
5
  import type { TitleData } from './types/title'
6
6
  import { getFullUrlPath } from './url'
7
+ import { getUserId, isUserTrackingAllowed } from './user'
7
8
 
8
9
  const baseUrl = 'https://insights.playpilot.net'
9
10
 
@@ -36,6 +37,10 @@ export async function track(event: string, title: TitleData | null = null, paylo
36
37
  }
37
38
  }
38
39
 
40
+ if (isUserTrackingAllowed()) {
41
+ payload.user_id = getUserId()
42
+ }
43
+
39
44
  payload.version = __SCRIPT_VERSION__
40
45
  payload.time_since_initialize = window.PlayPilotLinkInjections?.time_at_initialize ? Date.now() - window.PlayPilotLinkInjections.time_at_initialize : 0
41
46
  payload.url = getFullUrlPath()
@@ -70,6 +70,11 @@ export type ConfigResponse = {
70
70
  */
71
71
  categorize_playlinks?: boolean
72
72
 
73
+ /**
74
+ * By default no user identifiable data is tracked. This can be changed using this option. User ids are stored in localStorage and sent along with any tracking request.
75
+ */
76
+ allow_user_id_tracking?: boolean
77
+
73
78
  /**
74
79
  * The following options are all relevant for in text disclaimers, which renders as a disclaimer text within the article,
75
80
  * rather than only inside of title cards.
@@ -0,0 +1,21 @@
1
+ import { generateRandomHash } from './hash'
2
+
3
+ export const localStorageUserKey = 'playpilot_user_id'
4
+
5
+ export function getUserId(): string | null {
6
+ if (!isUserTrackingAllowed()) return null
7
+
8
+ try {
9
+ const userId = localStorage.getItem(localStorageUserKey)
10
+ if (userId) return userId
11
+
12
+ localStorage.setItem(localStorageUserKey, generateRandomHash(16))
13
+ return localStorage.getItem(localStorageUserKey)
14
+ } catch {
15
+ return null
16
+ }
17
+ }
18
+
19
+ export function isUserTrackingAllowed(): boolean {
20
+ return !!window.PlayPilotLinkInjections?.config?.allow_user_id_tracking
21
+ }
@@ -10,6 +10,7 @@
10
10
  const { embeddable_url = '', onclose }: Props = $props()
11
11
 
12
12
  const videoId = $derived(getVideoId(embeddable_url))
13
+ const color = window?.getComputedStyle(document.body).getPropertyValue('--playpilot-primary')?.replace('#', '') || 'fa548a'
13
14
 
14
15
  // Gets the YouTube ID from a url, can be a large number of different formats
15
16
  // https://stackoverflow.com/a/54200105/1665157
@@ -23,7 +24,7 @@
23
24
 
24
25
  <div class="overlay" transition:fade={{ duration: 100 }}>
25
26
  {#if videoId}
26
- <iframe width="600" height="338" src="https://www.youtube.com/embed/{videoId}?autoplay=true" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen></iframe>
27
+ <iframe width="600" height="338" src="https://video.playpilot.net/?video_id={videoId}&color={color}&autoplay=true&playsinline=true&controls=play,mute,fullscreen,progress,current-time" title="YouTube video player" frameborder="0" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen></iframe>
27
28
  {:else}
28
29
  Something went wrong
29
30
  {/if}
@@ -1,5 +1,5 @@
1
1
  import { describe, it, expect } from 'vitest'
2
- import { stringToHash } from '$lib/hash'
2
+ import { generateRandomHash, stringToHash } from '$lib/hash'
3
3
 
4
4
  describe('$lib/hash', () => {
5
5
  describe('stringToHash', () => {
@@ -25,4 +25,23 @@ describe('$lib/hash', () => {
25
25
  expect(() => stringToHash(longString)).not.toThrow()
26
26
  })
27
27
  })
28
+
29
+ describe('generateRandomHash', () => {
30
+ it('Should generate a random 5 character hash by default', () => {
31
+ expect(generateRandomHash()).toHaveLength(5)
32
+ })
33
+
34
+ it('Should generate a string of given length', () => {
35
+ expect(generateRandomHash(0)).toHaveLength(0)
36
+ expect(generateRandomHash(1)).toHaveLength(1)
37
+ expect(generateRandomHash(12)).toHaveLength(12)
38
+ expect(generateRandomHash(61)).toHaveLength(61)
39
+ expect(generateRandomHash(250)).toHaveLength(250)
40
+ })
41
+
42
+ it('Should generate random strings each time', () => {
43
+ expect(generateRandomHash()).not.toBe(generateRandomHash())
44
+ expect(generateRandomHash(10)).not.toBe(generateRandomHash(10))
45
+ })
46
+ })
28
47
  })
@@ -6,6 +6,7 @@ import { getFullUrlPath } from '$lib/url'
6
6
  import { fakeFetch } from '../helpers'
7
7
  import { hasConsentedTo } from '$lib/consent'
8
8
  import { isCrawler } from '$lib/crawler'
9
+ import { getUserId } from '$lib/user'
9
10
 
10
11
  vi.mock('$lib/consent', () => ({
11
12
  hasConsentedTo: vi.fn(() => true),
@@ -206,6 +207,32 @@ describe('$lib/tracking', () => {
206
207
  }),
207
208
  )
208
209
  })
210
+
211
+ it('Should include user_id if tracking is allowed', () => {
212
+ window.PlayPilotLinkInjections.config = { allow_user_id_tracking: true }
213
+
214
+ const userId = getUserId()
215
+
216
+ track('Some Event')
217
+ expect(global.fetch).toHaveBeenCalledWith(
218
+ expect.any(String),
219
+ expect.objectContaining({
220
+ body: expect.stringContaining(`"user_id":"${userId}"`),
221
+ }),
222
+ )
223
+ })
224
+
225
+ it('Should not include user_id if tracking is not allowed', () => {
226
+ window.PlayPilotLinkInjections.config = { allow_user_id_tracking: false }
227
+
228
+ track('Some Event')
229
+ expect(global.fetch).toHaveBeenCalledWith(
230
+ expect.any(String),
231
+ expect.objectContaining({
232
+ body: expect.not.stringContaining('user_id'),
233
+ }),
234
+ )
235
+ })
209
236
  })
210
237
 
211
238
  describe('setTrackingSids', () => {
@@ -0,0 +1,57 @@
1
+ import { describe, it, expect, beforeEach } from 'vitest'
2
+ import { getUserId, isUserTrackingAllowed, localStorageUserKey } from '$lib/user'
3
+
4
+ describe('user.ts', () => {
5
+ describe('getUserId', () => {
6
+ beforeEach(() => {
7
+ // @ts-ignore
8
+ window.PlayPilotLinkInjections = {
9
+ config: {
10
+ allow_user_id_tracking: true,
11
+ },
12
+ }
13
+ })
14
+
15
+ it('Should return random hash by default', () => {
16
+ expect(getUserId()).toBeTruthy()
17
+ })
18
+
19
+ it('Should return the previously set value', () => {
20
+ localStorage.setItem(localStorageUserKey, 'some_value')
21
+
22
+ expect(getUserId()).toBe('some_value')
23
+ })
24
+
25
+ it('Should return null if allow_user_id_tracking is not set', () => {
26
+ // @ts-ignore
27
+ window.PlayPilotLinkInjections = {}
28
+
29
+ expect(getUserId()).toBe(null)
30
+
31
+ localStorage.setItem(localStorageUserKey, 'some_value')
32
+
33
+ expect(getUserId()).toBe(null)
34
+ })
35
+
36
+ it('Should return the same id when called multiple times', () => {
37
+ expect(getUserId()).toBe(getUserId())
38
+ })
39
+ })
40
+
41
+ describe('isUserTrackingAllowed', () => {
42
+ beforeEach(() => {
43
+ // @ts-ignore
44
+ window.PlayPilotLinkInjections = {}
45
+ })
46
+
47
+ it('Should return true if user tracking is allowed', () => {
48
+ window.PlayPilotLinkInjections.config = { allow_user_id_tracking: true }
49
+
50
+ expect(isUserTrackingAllowed()).toBe(true)
51
+ })
52
+
53
+ it('Should return false if user tracking is not allowed', () => {
54
+ expect(isUserTrackingAllowed()).toBe(false)
55
+ })
56
+ })
57
+ })
@@ -8,7 +8,7 @@ describe('YouTubeEmbedOverlay.svelte', () => {
8
8
  const { container } = render(YouTubeEmbedOverlay, { embeddable_url: 'youtube.com/watch?v=abc', onclose: () => null })
9
9
 
10
10
  // @ts-ignore
11
- expect(container.querySelector('iframe').src).toBe('https://www.youtube.com/embed/abc?autoplay=true')
11
+ expect(container.querySelector('iframe').src).toBe('https://video.playpilot.net/?video_id=abc&autoplay=true&playsinline=true&controls=play,mute,fullscreen,progress,current-time&color=#fa548a')
12
12
  })
13
13
 
14
14
  it('Should render error message if embeddable_url is invalid', () => {