@thumbmarkjs/thumbmarkjs 1.5.1 → 1.6.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.
Files changed (53) hide show
  1. package/README.md +4 -4
  2. package/dist/thumbmark.cjs.js +1 -1
  3. package/dist/thumbmark.cjs.js.map +1 -1
  4. package/dist/thumbmark.esm.d.ts +19 -2
  5. package/dist/thumbmark.esm.js +1 -1
  6. package/dist/thumbmark.esm.js.map +1 -1
  7. package/dist/thumbmark.umd.js +1 -1
  8. package/dist/thumbmark.umd.js.map +1 -1
  9. package/dist/types/components/audio/index.d.ts +2 -0
  10. package/dist/types/components/canvas/index.d.ts +3 -0
  11. package/dist/types/components/fonts/index.d.ts +4 -0
  12. package/dist/types/components/hardware/index.d.ts +2 -0
  13. package/dist/types/components/locales/index.d.ts +2 -0
  14. package/dist/types/components/math/index.d.ts +2 -0
  15. package/dist/types/components/mathml/index.d.ts +2 -0
  16. package/dist/types/components/permissions/index.d.ts +3 -0
  17. package/dist/types/components/plugins/index.d.ts +2 -0
  18. package/dist/types/components/screen/index.d.ts +2 -0
  19. package/dist/types/components/speech/index.d.ts +2 -0
  20. package/dist/types/components/system/browser.d.ts +7 -0
  21. package/dist/types/components/system/index.d.ts +2 -0
  22. package/dist/types/components/webgl/index.d.ts +2 -0
  23. package/dist/types/components/webrtc/index.d.ts +2 -0
  24. package/dist/types/factory.d.ts +62 -0
  25. package/dist/types/functions/api.d.ts +50 -0
  26. package/dist/types/functions/filterComponents.d.ts +10 -0
  27. package/dist/types/functions/index.d.ts +45 -0
  28. package/dist/types/functions/legacy_functions.d.ts +27 -0
  29. package/dist/types/index.d.ts +9 -0
  30. package/dist/types/options.d.ts +59 -0
  31. package/dist/types/thumbmark.d.ts +26 -0
  32. package/dist/types/utils/cache.d.ts +23 -0
  33. package/dist/types/utils/commonPixels.d.ts +1 -0
  34. package/dist/types/utils/ephemeralIFrame.d.ts +4 -0
  35. package/dist/types/utils/getMostFrequent.d.ts +5 -0
  36. package/dist/types/utils/hash.d.ts +5 -0
  37. package/dist/types/utils/imageDataToDataURL.d.ts +1 -0
  38. package/dist/types/utils/log.d.ts +8 -0
  39. package/dist/types/utils/raceAll.d.ts +9 -0
  40. package/dist/types/utils/sort.d.ts +8 -0
  41. package/dist/types/utils/stableStringify.d.ts +22 -0
  42. package/dist/types/utils/version.d.ts +4 -0
  43. package/dist/types/utils/visitorId.d.ts +14 -0
  44. package/package.json +1 -1
  45. package/src/functions/api.test.ts +71 -0
  46. package/src/functions/api.ts +49 -5
  47. package/src/functions/index.ts +2 -2
  48. package/src/options.test.ts +10 -0
  49. package/src/options.ts +30 -4
  50. package/src/utils/cache.test.ts +94 -0
  51. package/src/utils/cache.ts +59 -0
  52. package/src/utils/visitorId.test.ts +12 -3
  53. package/src/utils/visitorId.ts +31 -8
@@ -0,0 +1,5 @@
1
+ /**
2
+ * This code is taken from https://github.com/LinusU/murmur-128/blob/master/index.js
3
+ * But instead of dependencies to encode-utf8 and fmix, I've implemented them here.
4
+ */
5
+ export declare function hash(key: ArrayBuffer | string, seed?: number): string;
@@ -0,0 +1 @@
1
+ export declare function imageDataToDataURL(imageData: ImageData): string;
@@ -0,0 +1,8 @@
1
+ import { componentInterface } from '../factory';
2
+ import { optionsInterface } from '../options';
3
+ /**
4
+ * Logs thumbmark data to remote logging endpoint (only once per session)
5
+ * You can disable this by setting options.logging to false.
6
+ * @internal
7
+ */
8
+ export declare function logThumbmarkData(thisHash: string, thumbmarkData: componentInterface, options: optionsInterface, experimentalData?: componentInterface): Promise<void>;
@@ -0,0 +1,9 @@
1
+ type DelayedPromise<T> = Promise<T>;
2
+ export declare function delay<T>(t: number, val: T): DelayedPromise<T>;
3
+ export interface RaceResult<T> {
4
+ value: T;
5
+ elapsed?: number;
6
+ }
7
+ export declare function raceAllPerformance<T>(promises: Promise<T>[], timeoutTime: number, timeoutVal: T): Promise<RaceResult<T>[]>;
8
+ export declare function raceAll<T>(promises: Promise<T>[], timeoutTime: number, timeoutVal: T): Promise<(T | undefined)[]>;
9
+ export {};
@@ -0,0 +1,8 @@
1
+ import { componentInterface } from '../factory';
2
+ /**
3
+ * Recursively sorts the keys of a component object alphabetically.
4
+ * This ensures a consistent order for hashing.
5
+ * @param obj The component object to sort.
6
+ * @returns A new object with sorted keys.
7
+ */
8
+ export declare function sortComponentKeys(obj: componentInterface): componentInterface;
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Stable JSON stringify implementation
3
+ * Based on fast-json-stable-stringify by Evgeny Poberezkin
4
+ * https://github.com/epoberezkin/fast-json-stable-stringify
5
+ *
6
+ * This implementation ensures consistent JSON serialization by sorting object keys,
7
+ * which is critical for generating stable hashes from fingerprint data.
8
+ */
9
+ /**
10
+ * Converts data to a stable JSON string with sorted keys
11
+ *
12
+ * @param data - The data to stringify
13
+ * @returns Stable JSON string representation
14
+ * @throws TypeError if circular reference is detected
15
+ *
16
+ * @example
17
+ * ```typescript
18
+ * const obj = { b: 2, a: 1 };
19
+ * stableStringify(obj); // '{"a":1,"b":2}'
20
+ * ```
21
+ */
22
+ export declare function stableStringify(data: any): string;
@@ -0,0 +1,4 @@
1
+ /**
2
+ * Returns the current package version
3
+ */
4
+ export declare function getVersion(): string;
@@ -0,0 +1,14 @@
1
+ import { OptionsAfterDefaults } from "../options";
2
+ /**
3
+ * Get the storage property name for visitor id
4
+ * @param _options
5
+ */
6
+ export declare function getVisitorIdPropertyName(_options: Pick<OptionsAfterDefaults, 'storage_property_name' | 'property_name_factory'>): string;
7
+ /**
8
+ * Gets visitor ID from localStorage, returns null if unavailable
9
+ */
10
+ export declare function getVisitorId(_options: OptionsAfterDefaults): string | null;
11
+ /**
12
+ * Sets visitor ID in localStorage
13
+ */
14
+ export declare function setVisitorId(visitorId: string, _options: OptionsAfterDefaults): void;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@thumbmarkjs/thumbmarkjs",
3
- "version": "1.5.1",
3
+ "version": "1.6.0",
4
4
  "description": "",
5
5
  "type": "module",
6
6
  "main": "./dist/thumbmark.cjs.js",
@@ -0,0 +1,71 @@
1
+ import {apiResponse, getCachedApiResponse, setCachedApiResponse} from "./api";
2
+ import { defaultOptions } from "../options";
3
+ import {getCache, setCache} from "../utils/cache";
4
+
5
+ const options = {
6
+ ...defaultOptions,
7
+ cache_lifetime_in_ms: 100,
8
+ }
9
+
10
+ const apiResponse = {
11
+ identifier: 'test'
12
+ } as apiResponse;
13
+
14
+ describe('setCachedApiResponse', () => {
15
+ beforeEach(() => {
16
+ localStorage.clear();
17
+ })
18
+
19
+ test('it should write the apiResponse to the cache if options allow that', () => {
20
+ setCachedApiResponse(options, apiResponse);
21
+ expect(getCache(options).apiResponse).toEqual(apiResponse);
22
+ });
23
+
24
+ test('it should not write if cache if off', () => {
25
+ setCachedApiResponse({
26
+ ...options,
27
+ cache_api_call: false,
28
+ }, apiResponse);
29
+
30
+ expect(getCache(options).apiResponse).not.toBeDefined();
31
+ expect(getCache(options).apiResponseExpiry).not.toBeDefined();
32
+ });
33
+
34
+ test('it should not write if lifetime is 0', () => {
35
+ setCachedApiResponse({
36
+ ...options,
37
+ cache_lifetime_in_ms: 0
38
+ }, apiResponse);
39
+
40
+ expect(getCache(options).apiResponse).not.toBeDefined();
41
+ expect(getCache(options).apiResponseExpiry).not.toBeDefined();
42
+ });
43
+ })
44
+
45
+ describe('getCachedApiResponse', () => {
46
+ beforeEach(() => {
47
+ localStorage.clear();
48
+ })
49
+
50
+ test('it should get from the cache if a value exists there', () => {
51
+ setCache(options, {
52
+ apiResponseExpiry: Date.now() + 2000000,
53
+ apiResponse,
54
+ });
55
+
56
+ const cached = getCachedApiResponse(options);
57
+ expect(cached).toBeDefined();
58
+ expect(cached).toEqual(apiResponse);
59
+ });
60
+
61
+ test('it should not return an expiried cache', () => {
62
+ setCache(options, {
63
+ apiResponseExpiry: Date.now() - 2000,
64
+ apiResponse,
65
+ });
66
+
67
+ const cached = getCachedApiResponse(options);
68
+ expect(cached).not.toBeDefined();
69
+ })
70
+
71
+ })
@@ -1,9 +1,10 @@
1
- import { optionsInterface, DEFAULT_API_ENDPOINT } from '../options';
1
+ import { optionsInterface, DEFAULT_API_ENDPOINT, OptionsAfterDefaults} from '../options';
2
2
  import { componentInterface } from '../factory';
3
3
  import { getVisitorId, setVisitorId } from '../utils/visitorId';
4
4
  import { getVersion } from "../utils/version";
5
5
  import { hash } from '../utils/hash';
6
6
  import { stableStringify } from '../utils/stableStringify';
7
+ import { getCache, getApiResponseExpiry, setCache } from "../utils/cache";
7
8
 
8
9
  // ===================== Types & Interfaces =====================
9
10
 
@@ -33,7 +34,7 @@ export interface infoInterface {
33
34
  /**
34
35
  * API response structure
35
36
  */
36
- interface apiResponse {
37
+ export interface apiResponse {
37
38
  info?: infoInterface;
38
39
  version?: string;
39
40
  components?: componentInterface;
@@ -51,12 +52,21 @@ let apiPromiseResult: apiResponse | null = null;
51
52
  * Returns a promise for the API response or null on error.
52
53
  */
53
54
  export const getApiPromise = (
54
- options: optionsInterface,
55
+ options: OptionsAfterDefaults,
55
56
  components: componentInterface
56
57
  ): Promise<apiResponse | null> => {
57
58
  // 1. If a result is already cached and caching is enabled, return it.
58
- if (options.cache_api_call && apiPromiseResult) {
59
- return Promise.resolve(apiPromiseResult);
59
+ if (options.cache_api_call) {
60
+ // Check the in-memory cache
61
+ if(apiPromiseResult) {
62
+ return Promise.resolve(apiPromiseResult);
63
+ }
64
+
65
+ // Check the localStorage cache
66
+ const cached = getCachedApiResponse(options);
67
+ if(cached) {
68
+ return Promise.resolve(cached);
69
+ }
60
70
  }
61
71
 
62
72
  // 2. If a request is already in flight, return that promise to prevent duplicate calls.
@@ -103,6 +113,7 @@ export const getApiPromise = (
103
113
  setVisitorId(data.visitorId, options);
104
114
  }
105
115
  apiPromiseResult = data; // Cache the successful result
116
+ setCachedApiResponse(options, data); // Cache to localStorage according to options
106
117
  currentApiPromise = null; // Clear the in-flight promise
107
118
  return data;
108
119
  })
@@ -131,3 +142,36 @@ export const getApiPromise = (
131
142
  currentApiPromise = Promise.race([fetchPromise, timeoutPromise]);
132
143
  return currentApiPromise;
133
144
  };
145
+
146
+ /**
147
+ * If a valid cached api response exists, returns it
148
+ * @param options
149
+ */
150
+ export function getCachedApiResponse(
151
+ options: Pick<OptionsAfterDefaults, 'property_name_factory'>,
152
+ ): apiResponse | undefined {
153
+ const cache = getCache(options);
154
+ if (cache && cache.apiResponse && cache.apiResponseExpiry && Date.now() <= cache.apiResponseExpiry) {
155
+ return cache.apiResponse;
156
+ }
157
+
158
+ return;
159
+ }
160
+
161
+ /**
162
+ * Writes the api response to the cache according to the options
163
+ * @param options
164
+ * @param response
165
+ */
166
+ export function setCachedApiResponse(
167
+ options: Pick<OptionsAfterDefaults, 'cache_api_call' | 'cache_lifetime_in_ms' | 'property_name_factory'>, response: apiResponse
168
+ ): void {
169
+ if(!options.cache_api_call || !options.cache_lifetime_in_ms) {
170
+ return;
171
+ }
172
+
173
+ setCache(options, {
174
+ apiResponseExpiry: getApiResponseExpiry(options),
175
+ apiResponse: response,
176
+ });
177
+ }
@@ -6,7 +6,7 @@
6
6
  *
7
7
  */
8
8
 
9
- import { defaultOptions, optionsInterface } from "../options";
9
+ import {defaultOptions, OptionsAfterDefaults, optionsInterface} from "../options";
10
10
  import {
11
11
  timeoutInstance,
12
12
  componentInterface,
@@ -45,7 +45,7 @@ interface thumbmarkResponse {
45
45
  * @returns thumbmarkResponse (elapsed is present only if options.performance is true)
46
46
  */
47
47
  export async function getThumbmark(options?: optionsInterface): Promise<thumbmarkResponse> {
48
- const _options = { ...defaultOptions, ...options };
48
+ const _options = { ...defaultOptions, ...options } as OptionsAfterDefaults;
49
49
 
50
50
  // Early logging decision
51
51
  const shouldLog = (_options.logging && !sessionStorage.getItem("_tmjs_l") && Math.random() < 0.0001);
@@ -0,0 +1,10 @@
1
+ import { DEFAULT_STORAGE_PREFIX, defaultOptions } from "./options";
2
+
3
+ describe('property_name_factory', () => {
4
+ test('it should default to the default value', () => {
5
+ const name = 'mykey';
6
+ expect(
7
+ defaultOptions.property_name_factory(name)
8
+ ).toEqual(`${DEFAULT_STORAGE_PREFIX}_${name}`);
9
+ })
10
+ })
package/src/options.ts CHANGED
@@ -1,4 +1,13 @@
1
- export interface optionsInterface {
1
+ export interface OptionsAfterDefaults {
2
+ /**
3
+ * A function to customise localStorage names used by thumbmark
4
+ * @param name Original name of the storage property eg. visitor_id
5
+ * @returns The name under which the storage property should be saved eg. myprefix_visitor_id
6
+ */
7
+ property_name_factory: (name: string) => string,
8
+ /**
9
+ * @deprecated use property_name_factory
10
+ */
2
11
  storage_property_name?: string,
3
12
  exclude?: string[],
4
13
  include?: string[],
@@ -7,32 +16,49 @@ export interface optionsInterface {
7
16
  logging?: boolean,
8
17
  api_key?: string,
9
18
  api_endpoint?: string,
19
+ /**
20
+ * @deprecated This will be removed in Thumbmarkjs 2.0, use cache_lifetime_in_ms instead
21
+ */
10
22
  cache_api_call?: boolean,
23
+ /**
24
+ * How long the cache will be valid for, maximum is 72h (259_200_000)
25
+ */
26
+ cache_lifetime_in_ms: number,
11
27
  performance?: boolean,
12
28
  stabilize?: string[],
13
29
  experimental?: boolean,
14
30
  }
15
31
 
32
+ export type optionsInterface = Partial<OptionsAfterDefaults>;
33
+
34
+ // Default to zero to avoid breaking existing integrations
35
+ export const DEFAULT_CACHE_LIFETIME = 0;
36
+ export const MAXIMUM_CACHE_LIFETIME = 259_200_000;
37
+ export const DEFAULT_STORAGE_PREFIX = 'thumbmark';
16
38
  export const DEFAULT_API_ENDPOINT = 'https://api.thumbmarkjs.com';
17
39
 
18
- export const defaultOptions: optionsInterface = {
40
+ export const defaultOptions: OptionsAfterDefaults = {
19
41
  exclude: [],
20
42
  include: [],
21
43
  stabilize: ['private', 'iframe'],
22
44
  logging: true,
23
45
  timeout: 5000,
24
46
  cache_api_call: true,
47
+ cache_lifetime_in_ms: DEFAULT_CACHE_LIFETIME,
25
48
  performance: false,
26
49
  experimental: false,
50
+ property_name_factory: (name: string) => {
51
+ return `${DEFAULT_STORAGE_PREFIX}_${name}`;
52
+ },
27
53
  };
28
54
 
29
- export let options = { ...defaultOptions };
55
+ export let options: OptionsAfterDefaults = { ...defaultOptions };
30
56
  /**
31
57
  *
32
58
  * @param key @deprecated this function will be removed
33
59
  * @param value
34
60
  */
35
- export function setOption<K extends keyof optionsInterface>(key: K, value: optionsInterface[K]) {
61
+ export function setOption<K extends keyof optionsInterface>(key: K, value: OptionsAfterDefaults[K]) {
36
62
  options[key] = value;
37
63
  }
38
64
 
@@ -0,0 +1,94 @@
1
+ import { Cache, getCache, setCache, getApiResponseExpiry, CACHE_KEY } from "./cache";
2
+ import { DEFAULT_STORAGE_PREFIX, defaultOptions, MAXIMUM_CACHE_LIFETIME } from "../options";
3
+
4
+ const _options = {
5
+ property_name_factory: (name: string) => {
6
+ return `${name}-mypostfix`;
7
+ },
8
+ cache_lifetime_in_ms: 0,
9
+ };
10
+
11
+ const values: Cache = {
12
+ apiResponseExpiry: 100,
13
+ apiResponse: {
14
+ version: '1.2.3',
15
+ },
16
+ };
17
+
18
+ const cacheProperty = defaultOptions.property_name_factory(CACHE_KEY);
19
+ const customCacheProperty = _options.property_name_factory(CACHE_KEY);
20
+
21
+ describe('getCache', () => {
22
+ beforeEach(() => {
23
+ localStorage.clear();
24
+ });
25
+
26
+ test('it should return all values from cache', () => {
27
+ localStorage.setItem(cacheProperty, JSON.stringify(values));
28
+ const fromStorage = getCache(defaultOptions);
29
+
30
+ expect(fromStorage).toEqual(values);
31
+ });
32
+
33
+ test('it should return an empty object in case of nothing cached', () => {
34
+ expect(getCache(defaultOptions)).toEqual({});
35
+ });
36
+
37
+ test('it should return an empty object in case non-json content', () => {
38
+ localStorage.setItem(cacheProperty, 'abc123');
39
+ expect(getCache(defaultOptions)).toEqual({});
40
+ });
41
+
42
+ test('it should return from the correct property', () => {
43
+ localStorage.setItem(cacheProperty, JSON.stringify(values));
44
+
45
+ expect(getCache(_options)).toEqual({});
46
+ expect(getCache(defaultOptions)).toEqual(values);
47
+ });
48
+ })
49
+
50
+ describe('setCache', () => {
51
+ beforeEach(() => {
52
+ localStorage.clear();
53
+ });
54
+
55
+ test('it should write given values', () => {
56
+ setCache(defaultOptions, values);
57
+ expect(JSON.parse(localStorage.getItem(cacheProperty)!))
58
+ .toEqual(values);
59
+ });
60
+
61
+ test('it should not touch values that are not provided', () => {
62
+ setCache(_options, values);
63
+ setCache(_options, {
64
+ apiResponseExpiry: 200
65
+ });
66
+ const fromStorage = JSON.parse(localStorage.getItem(customCacheProperty)!) as Cache;
67
+ expect(fromStorage.apiResponseExpiry).toEqual(200);
68
+ expect(fromStorage.apiResponse!.version).toEqual(values.apiResponse!.version);
69
+ })
70
+ })
71
+
72
+ describe('getApiResponseExpiry', () => {
73
+ const systemTime = 1000;
74
+ beforeAll(() => {
75
+ jest.useFakeTimers().setSystemTime(systemTime);
76
+ localStorage.clear();
77
+ });
78
+
79
+ test('it should return a value based on options', () => {
80
+ expect(getApiResponseExpiry({
81
+ cache_lifetime_in_ms: 100,
82
+ })).toBe(systemTime + 100);
83
+ });
84
+
85
+ test('it should not go over maximum cache lifetime', () => {
86
+ expect(getApiResponseExpiry({
87
+ cache_lifetime_in_ms: MAXIMUM_CACHE_LIFETIME + 200,
88
+ })).toBe(systemTime + MAXIMUM_CACHE_LIFETIME);
89
+ })
90
+
91
+ afterAll(() => {
92
+ jest.useRealTimers()
93
+ })
94
+ })
@@ -0,0 +1,59 @@
1
+ import { apiResponse } from "../functions/api";
2
+ import { MAXIMUM_CACHE_LIFETIME, OptionsAfterDefaults } from "../options";
3
+
4
+ export const CACHE_KEY = 'cache';
5
+
6
+ export interface Cache {
7
+ apiResponse?: apiResponse
8
+ apiResponseExpiry?: number;
9
+ }
10
+
11
+ /**
12
+ * Get all values from cache
13
+ * @param _options
14
+ */
15
+ export function getCache(_options: Pick<OptionsAfterDefaults, 'property_name_factory'>): Cache {
16
+ try {
17
+ const rawCache = localStorage.getItem(_options.property_name_factory(CACHE_KEY));
18
+ const jsonCache = JSON.parse(rawCache!);
19
+ if(!jsonCache) {
20
+ return {};
21
+ } else {
22
+ return jsonCache as Cache;
23
+ }
24
+ } catch {
25
+ // Ignore storage errors
26
+ }
27
+
28
+ return {};
29
+ }
30
+
31
+ /**
32
+ * Write given values to cache
33
+ * @param _options
34
+ * @param values
35
+ */
36
+ export function setCache(_options: OptionsAfterDefaults, values: Partial<Cache>): void {
37
+ const newValues: Cache = {
38
+ ...getCache(_options),
39
+ ...values
40
+ };
41
+
42
+ try {
43
+ localStorage.setItem(_options.property_name_factory(CACHE_KEY), JSON.stringify(newValues));
44
+ } catch {
45
+ // Ignore storage errors
46
+ }
47
+ }
48
+
49
+ /**
50
+ * Returns the expiry time for cache
51
+ * @param _options
52
+ */
53
+ export function getApiResponseExpiry(_options: Pick<OptionsAfterDefaults, 'cache_lifetime_in_ms'>): number {
54
+ if(_options.cache_lifetime_in_ms > MAXIMUM_CACHE_LIFETIME) {
55
+ return Date.now() + MAXIMUM_CACHE_LIFETIME;
56
+ }
57
+
58
+ return Date.now() + _options.cache_lifetime_in_ms;
59
+ }
@@ -1,5 +1,5 @@
1
1
  import { getVisitorId, setVisitorId } from './visitorId';
2
- import { optionsInterface, defaultOptions } from '../options';
2
+ import { defaultOptions, options, OptionsAfterDefaults } from '../options';
3
3
 
4
4
  describe('visitorId storage tests', () => {
5
5
  beforeEach(() => {
@@ -21,7 +21,7 @@ describe('visitorId storage tests', () => {
21
21
 
22
22
  test('should use custom storage property name', () => {
23
23
  const visitorId = 'custom-visitor-456';
24
- const customOptions: optionsInterface = {
24
+ const customOptions: OptionsAfterDefaults = {
25
25
  ...defaultOptions,
26
26
  storage_property_name: 'my_custom_visitor_key'
27
27
  };
@@ -37,7 +37,7 @@ describe('visitorId storage tests', () => {
37
37
  });
38
38
 
39
39
  test('should return null when storage property does not exist', () => {
40
- const options: optionsInterface = {
40
+ const options: OptionsAfterDefaults = {
41
41
  ...defaultOptions,
42
42
  storage_property_name: 'nonexistent_key'
43
43
  };
@@ -56,6 +56,15 @@ describe('visitorId storage tests', () => {
56
56
  setVisitorId(newVisitorId, options);
57
57
  expect(getVisitorId(options)).toBe(newVisitorId);
58
58
  });
59
+
60
+ test('should migrate from old value in case new prefix is set', () => {
61
+ const visitorId = 'test-visitor-123';
62
+ setVisitorId(visitorId, options);
63
+ expect(getVisitorId({
64
+ property_name_factory: (name) => `custom_prefix_${name}`,
65
+ } as OptionsAfterDefaults)).toBe(visitorId);
66
+ expect(localStorage.getItem(`custom_prefix_visitor_id`)).toBe(visitorId);
67
+ })
59
68
  });
60
69
 
61
70
  describe('error handling', () => {
@@ -1,18 +1,42 @@
1
- import { optionsInterface } from "../options";
1
+ import { DEFAULT_STORAGE_PREFIX, OptionsAfterDefaults } from "../options";
2
2
 
3
3
  /**
4
4
  * Visitor ID storage utilities - localStorage only, server generates IDs
5
5
  */
6
6
 
7
- const DEFAULT_STORAGE_PROPERTY_NAME = 'thumbmark_visitor_id';
7
+ const DEFAULT_STORAGE_PROPERTY_NAME = 'visitor_id';
8
8
 
9
+ /**
10
+ * Get the storage property name for visitor id
11
+ * @param _options
12
+ */
13
+ export function getVisitorIdPropertyName(
14
+ _options: Pick<OptionsAfterDefaults, 'storage_property_name' | 'property_name_factory'>
15
+ ): string {
16
+ if(_options.storage_property_name) {
17
+ return _options.storage_property_name;
18
+ }
19
+
20
+ return _options.property_name_factory(DEFAULT_STORAGE_PROPERTY_NAME);
21
+ }
22
+
23
+ const DEFAULT_VISITOR_ID_NAME = `${DEFAULT_STORAGE_PREFIX}_${DEFAULT_STORAGE_PROPERTY_NAME}`;
9
24
  /**
10
25
  * Gets visitor ID from localStorage, returns null if unavailable
11
26
  */
12
- export function getVisitorId(_options: optionsInterface): string | null {
13
- const storagePropertyName = _options.storage_property_name || DEFAULT_STORAGE_PROPERTY_NAME;
27
+ export function getVisitorId(_options: OptionsAfterDefaults): string | null {
14
28
  try {
15
- return localStorage.getItem(storagePropertyName);
29
+ const propertyName = getVisitorIdPropertyName(_options);
30
+ let visitorId = localStorage.getItem(propertyName);
31
+ if(!visitorId && propertyName !== DEFAULT_VISITOR_ID_NAME) {
32
+ // Migration case in case going from thumbmark prefix to a custom one
33
+ visitorId = localStorage.getItem(DEFAULT_VISITOR_ID_NAME);
34
+ if(visitorId) {
35
+ setVisitorId(visitorId, _options);
36
+ }
37
+ }
38
+
39
+ return visitorId;
16
40
  } catch {
17
41
  return null;
18
42
  }
@@ -21,10 +45,9 @@ export function getVisitorId(_options: optionsInterface): string | null {
21
45
  /**
22
46
  * Sets visitor ID in localStorage
23
47
  */
24
- export function setVisitorId(visitorId: string, _options: optionsInterface): void {
25
- const storagePropertyName = _options.storage_property_name || DEFAULT_STORAGE_PROPERTY_NAME;
48
+ export function setVisitorId(visitorId: string, _options: OptionsAfterDefaults): void {
26
49
  try {
27
- localStorage.setItem(storagePropertyName, visitorId);
50
+ localStorage.setItem(getVisitorIdPropertyName(_options), visitorId);
28
51
  } catch {
29
52
  // Ignore storage errors
30
53
  }