@croct/sdk 0.10.0 → 0.11.0-alpha.1

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 (231) hide show
  1. package/.src/activeRecord.ts +150 -0
  2. package/.src/base64Url.ts +18 -0
  3. package/.src/cache/cache.ts +15 -0
  4. package/.src/cache/fallbackCache.ts +29 -0
  5. package/.src/cache/inMemoryCache.ts +21 -0
  6. package/.src/cache/index.ts +4 -0
  7. package/.src/cache/localStorageCache.ts +85 -0
  8. package/.src/channel/beaconSocketChannel.ts +153 -0
  9. package/.src/channel/channel.ts +20 -0
  10. package/.src/channel/encodedChannel.ts +21 -0
  11. package/.src/channel/guaranteedChannel.ts +131 -0
  12. package/.src/channel/index.ts +8 -0
  13. package/.src/channel/queuedChannel.ts +112 -0
  14. package/.src/channel/retryChannel.ts +90 -0
  15. package/.src/channel/sandboxChannel.ts +43 -0
  16. package/.src/channel/socketChannel.ts +217 -0
  17. package/.src/cid/assigner.ts +3 -0
  18. package/.src/cid/cachedAssigner.ts +35 -0
  19. package/.src/cid/fixedAssigner.ts +13 -0
  20. package/.src/cid/index.ts +4 -0
  21. package/.src/cid/remoteAssigner.ts +47 -0
  22. package/.src/constants.ts +6 -0
  23. package/.src/container.ts +388 -0
  24. package/.src/contentFetcher.ts +226 -0
  25. package/.src/context.ts +137 -0
  26. package/.src/error.ts +31 -0
  27. package/.src/evaluator.ts +251 -0
  28. package/.src/eventManager.ts +53 -0
  29. package/.src/facade/contentFetcherFacade.ts +69 -0
  30. package/.src/facade/evaluatorFacade.ts +152 -0
  31. package/.src/facade/index.ts +7 -0
  32. package/.src/facade/sdkFacade.ts +291 -0
  33. package/.src/facade/sessionFacade.ts +14 -0
  34. package/.src/facade/sessionPatch.ts +32 -0
  35. package/.src/facade/trackerFacade.ts +98 -0
  36. package/.src/facade/userFacade.ts +26 -0
  37. package/.src/facade/userPatch.ts +32 -0
  38. package/.src/index.ts +4 -0
  39. package/.src/logging/consoleLogger.ts +37 -0
  40. package/.src/logging/index.ts +4 -0
  41. package/.src/logging/logger.ts +13 -0
  42. package/.src/logging/namespacedLogger.ts +32 -0
  43. package/.src/logging/nullLogger.ts +19 -0
  44. package/.src/namespacedStorage.ts +69 -0
  45. package/.src/patch.ts +64 -0
  46. package/.src/queue/capacityRestrictedQueue.ts +44 -0
  47. package/.src/queue/inMemoryQueue.ts +43 -0
  48. package/.src/queue/index.ts +5 -0
  49. package/.src/queue/monitoredQueue.ts +168 -0
  50. package/.src/queue/persistentQueue.ts +84 -0
  51. package/.src/queue/queue.ts +15 -0
  52. package/.src/retry/arbitraryPolicy.ts +21 -0
  53. package/.src/retry/backoffPolicy.ts +84 -0
  54. package/.src/retry/index.ts +5 -0
  55. package/.src/retry/maxAttemptsPolicy.ts +28 -0
  56. package/.src/retry/neverPolicy.ts +11 -0
  57. package/.src/retry/policy.ts +5 -0
  58. package/.src/schema/attributeSchema.ts +6 -0
  59. package/.src/schema/contentFetcherSchemas.ts +23 -0
  60. package/.src/schema/contentSchemas.ts +44 -0
  61. package/.src/schema/contextSchemas.ts +5 -0
  62. package/.src/schema/ecommerceSchemas.ts +179 -0
  63. package/.src/schema/evaluatorSchemas.ts +11 -0
  64. package/.src/schema/eventSchemas.ts +150 -0
  65. package/.src/schema/index.ts +11 -0
  66. package/.src/schema/loggerSchema.ts +12 -0
  67. package/.src/schema/operationSchemas.ts +102 -0
  68. package/.src/schema/sdkFacadeSchemas.ts +44 -0
  69. package/.src/schema/sdkSchemas.ts +49 -0
  70. package/.src/schema/tokenSchema.ts +42 -0
  71. package/.src/schema/userSchema.ts +184 -0
  72. package/.src/sdk.ts +174 -0
  73. package/.src/sdkEvents.ts +15 -0
  74. package/.src/sourceLocation.ts +85 -0
  75. package/.src/tab.ts +148 -0
  76. package/.src/token/cachedTokenStore.ts +34 -0
  77. package/.src/token/inMemoryTokenStore.ts +13 -0
  78. package/.src/token/index.ts +4 -0
  79. package/.src/token/replicatedTokenStore.ts +21 -0
  80. package/.src/token/token.ts +164 -0
  81. package/.src/tracker.ts +460 -0
  82. package/.src/trackingEvents.ts +456 -0
  83. package/.src/transformer.ts +7 -0
  84. package/.src/utilityTypes.ts +3 -0
  85. package/.src/uuid.ts +43 -0
  86. package/.src/validation/arrayType.ts +71 -0
  87. package/.src/validation/booleanType.ts +22 -0
  88. package/.src/validation/functionType.ts +22 -0
  89. package/.src/validation/index.ts +12 -0
  90. package/.src/validation/jsonType.ts +157 -0
  91. package/.src/validation/mixedSchema.ts +7 -0
  92. package/.src/validation/nullType.ts +22 -0
  93. package/.src/validation/numberType.ts +59 -0
  94. package/.src/validation/objectType.ts +138 -0
  95. package/.src/validation/schema.ts +21 -0
  96. package/.src/validation/stringType.ts +118 -0
  97. package/.src/validation/unionType.ts +53 -0
  98. package/.src/validation/violation.ts +23 -0
  99. package/activeRecord.js +33 -36
  100. package/base64Url.js +1 -0
  101. package/cache/cache.js +1 -0
  102. package/cache/fallbackCache.js +16 -32
  103. package/cache/inMemoryCache.js +10 -10
  104. package/cache/index.js +2 -1
  105. package/cache/localStorageCache.js +25 -25
  106. package/channel/beaconSocketChannel.d.ts +1 -1
  107. package/channel/beaconSocketChannel.js +50 -79
  108. package/channel/channel.d.ts +1 -1
  109. package/channel/channel.js +1 -0
  110. package/channel/encodedChannel.js +9 -10
  111. package/channel/guaranteedChannel.d.ts +4 -4
  112. package/channel/guaranteedChannel.js +42 -43
  113. package/channel/index.js +2 -1
  114. package/channel/queuedChannel.js +36 -64
  115. package/channel/retryChannel.d.ts +1 -1
  116. package/channel/retryChannel.js +45 -77
  117. package/channel/sandboxChannel.js +18 -18
  118. package/channel/socketChannel.d.ts +4 -4
  119. package/channel/socketChannel.js +78 -79
  120. package/cid/assigner.js +1 -0
  121. package/cid/cachedAssigner.js +16 -27
  122. package/cid/fixedAssigner.js +6 -6
  123. package/cid/index.js +2 -1
  124. package/cid/remoteAssigner.js +24 -36
  125. package/constants.d.ts +6 -5
  126. package/constants.js +7 -5
  127. package/container.d.ts +13 -6
  128. package/container.js +153 -168
  129. package/contentFetcher.d.ts +59 -0
  130. package/contentFetcher.js +130 -0
  131. package/context.d.ts +3 -3
  132. package/context.js +37 -38
  133. package/error.js +3 -2
  134. package/evaluator.d.ts +33 -24
  135. package/evaluator.js +127 -117
  136. package/eventManager.d.ts +1 -1
  137. package/eventManager.js +15 -15
  138. package/facade/contentFetcherFacade.d.ts +27 -0
  139. package/facade/contentFetcherFacade.js +41 -0
  140. package/facade/evaluatorFacade.d.ts +13 -3
  141. package/facade/evaluatorFacade.js +58 -72
  142. package/facade/index.js +1 -0
  143. package/facade/sdkFacade.d.ts +10 -3
  144. package/facade/sdkFacade.js +130 -141
  145. package/facade/sessionFacade.js +7 -7
  146. package/facade/sessionPatch.js +10 -13
  147. package/facade/trackerFacade.js +33 -38
  148. package/facade/userFacade.js +11 -11
  149. package/facade/userPatch.js +10 -13
  150. package/index.js +3 -2
  151. package/logging/consoleLogger.js +19 -35
  152. package/logging/index.js +2 -1
  153. package/logging/logger.js +1 -0
  154. package/logging/namespacedLogger.js +15 -15
  155. package/logging/nullLogger.js +11 -13
  156. package/namespacedStorage.js +31 -47
  157. package/package.json +13 -16
  158. package/patch.d.ts +1 -1
  159. package/patch.js +1 -0
  160. package/queue/capacityRestrictedQueue.js +18 -18
  161. package/queue/inMemoryQueue.js +23 -28
  162. package/queue/index.js +2 -1
  163. package/queue/monitoredQueue.d.ts +2 -2
  164. package/queue/monitoredQueue.js +40 -40
  165. package/queue/persistentQueue.js +34 -38
  166. package/queue/queue.js +1 -0
  167. package/retry/arbitraryPolicy.js +9 -10
  168. package/retry/backoffPolicy.d.ts +1 -1
  169. package/retry/backoffPolicy.js +12 -13
  170. package/retry/index.js +2 -1
  171. package/retry/maxAttemptsPolicy.js +8 -8
  172. package/retry/neverPolicy.js +7 -9
  173. package/retry/policy.js +1 -0
  174. package/schema/attributeSchema.js +2 -1
  175. package/schema/contentFetcherSchemas.d.ts +2 -0
  176. package/schema/contentFetcherSchemas.js +23 -0
  177. package/schema/contentSchemas.js +2 -1
  178. package/schema/contextSchemas.js +2 -1
  179. package/schema/ecommerceSchemas.js +2 -1
  180. package/schema/evaluatorSchemas.d.ts +2 -0
  181. package/schema/{evaluationSchemas.js → evaluatorSchemas.js} +4 -3
  182. package/schema/eventSchemas.js +6 -7
  183. package/schema/index.d.ts +2 -1
  184. package/schema/index.js +4 -2
  185. package/schema/loggerSchema.js +2 -1
  186. package/schema/operationSchemas.js +9 -8
  187. package/schema/sdkFacadeSchemas.js +10 -6
  188. package/schema/sdkSchemas.js +9 -5
  189. package/schema/tokenSchema.js +6 -4
  190. package/schema/userSchema.js +3 -2
  191. package/sdk.d.ts +9 -3
  192. package/sdk.js +82 -127
  193. package/sdkEvents.d.ts +3 -3
  194. package/sdkEvents.js +1 -0
  195. package/sourceLocation.d.ts +3 -3
  196. package/sourceLocation.js +14 -14
  197. package/tab.d.ts +5 -5
  198. package/tab.js +51 -80
  199. package/token/cachedTokenStore.js +10 -10
  200. package/token/inMemoryTokenStore.js +8 -8
  201. package/token/index.js +2 -1
  202. package/token/replicatedTokenStore.js +8 -8
  203. package/token/token.d.ts +9 -5
  204. package/token/token.js +64 -57
  205. package/tracker.d.ts +4 -4
  206. package/tracker.js +146 -122
  207. package/trackingEvents.d.ts +36 -36
  208. package/trackingEvents.js +13 -6
  209. package/transformer.js +2 -1
  210. package/utilityTypes.d.ts +2 -2
  211. package/utilityTypes.js +1 -0
  212. package/uuid.js +10 -7
  213. package/validation/arrayType.d.ts +2 -2
  214. package/validation/arrayType.js +30 -27
  215. package/validation/booleanType.js +12 -15
  216. package/validation/functionType.js +12 -15
  217. package/validation/index.js +2 -1
  218. package/validation/jsonType.d.ts +2 -2
  219. package/validation/jsonType.js +62 -80
  220. package/validation/mixedSchema.js +5 -7
  221. package/validation/nullType.js +12 -15
  222. package/validation/numberType.d.ts +1 -1
  223. package/validation/numberType.js +24 -22
  224. package/validation/objectType.d.ts +1 -1
  225. package/validation/objectType.js +62 -72
  226. package/validation/schema.js +7 -10
  227. package/validation/stringType.d.ts +1 -1
  228. package/validation/stringType.js +37 -47
  229. package/validation/unionType.js +28 -77
  230. package/validation/violation.js +2 -2
  231. package/schema/evaluationSchemas.d.ts +0 -2
@@ -0,0 +1,164 @@
1
+ import {JsonObject} from '@croct/json';
2
+ import {base64UrlDecode, base64UrlEncode} from '../base64Url';
3
+ import {tokenSchema} from '../schema';
4
+ import {formatCause} from '../error';
5
+
6
+ export type Headers = {
7
+ typ: string,
8
+ alg: string,
9
+ kid?: string,
10
+ appId?: string,
11
+ };
12
+
13
+ type Claims = {
14
+ iss: string,
15
+ aud: string|string[],
16
+ iat: number,
17
+ exp?: number,
18
+ sub?: string,
19
+ jid?: string,
20
+ };
21
+
22
+ export type TokenPayload = JsonObject & Claims;
23
+
24
+ export class Token {
25
+ private readonly headers: Headers;
26
+
27
+ private readonly payload: TokenPayload;
28
+
29
+ private readonly signature: string;
30
+
31
+ private constructor(headers: Headers, payload: TokenPayload, signature = '') {
32
+ this.headers = headers;
33
+ this.payload = payload;
34
+ this.signature = signature;
35
+ }
36
+
37
+ public static issue(
38
+ appId: string,
39
+ subject: string|null = null,
40
+ timestamp: number = Math.floor(Date.now() / 1000),
41
+ ): Token {
42
+ if (timestamp < 0) {
43
+ throw new Error('The timestamp must be non-negative.');
44
+ }
45
+
46
+ if (subject === '') {
47
+ throw new Error('The subject must be non-empty.');
48
+ }
49
+
50
+ return new Token(
51
+ {
52
+ typ: 'JWT',
53
+ alg: 'none',
54
+ appId: appId,
55
+ },
56
+ {
57
+ iss: 'croct.io',
58
+ aud: 'croct.io',
59
+ iat: timestamp,
60
+ ...(subject !== null ? {sub: subject} : null),
61
+ },
62
+ );
63
+ }
64
+
65
+ public static parse(token: string): Token {
66
+ if (token === '') {
67
+ throw new Error('The token cannot be empty.');
68
+ }
69
+
70
+ const parts = token.split('.', 3);
71
+
72
+ // This token is invalid
73
+ if (parts.length < 2) {
74
+ throw new Error('The token is malformed.');
75
+ }
76
+
77
+ let headers;
78
+ let payload;
79
+ let signature;
80
+
81
+ try {
82
+ headers = JSON.parse(base64UrlDecode(parts[0]));
83
+ payload = JSON.parse(base64UrlDecode(parts[1]));
84
+
85
+ if (parts.length === 3) {
86
+ signature = base64UrlDecode(parts[2]);
87
+ }
88
+ } catch {
89
+ throw new Error('The token is corrupted.');
90
+ }
91
+
92
+ return Token.of(headers, payload, signature);
93
+ }
94
+
95
+ public static of(headers: Headers, payload: TokenPayload, signature = ''): Token {
96
+ try {
97
+ tokenSchema.validate({
98
+ headers: headers,
99
+ payload: payload,
100
+ signature: signature,
101
+ });
102
+ } catch (violation) {
103
+ throw new Error(`The token is invalid: ${formatCause(violation)}`);
104
+ }
105
+
106
+ return new Token(headers as Headers, payload as TokenPayload, signature as string);
107
+ }
108
+
109
+ public getHeaders(): Headers {
110
+ return {...this.headers};
111
+ }
112
+
113
+ public getPayload(): TokenPayload {
114
+ return {...this.payload};
115
+ }
116
+
117
+ public getSignature(): string {
118
+ return this.signature;
119
+ }
120
+
121
+ public isAnonymous(): boolean {
122
+ return this.payload.sub === undefined;
123
+ }
124
+
125
+ public getSubject(): string | null {
126
+ return this.payload.sub !== undefined ? this.payload.sub : null;
127
+ }
128
+
129
+ public getIssueTime(): number {
130
+ return this.payload.iat;
131
+ }
132
+
133
+ public toJSON(): string {
134
+ return this.toString();
135
+ }
136
+
137
+ public toString(): string {
138
+ const headers = base64UrlEncode(JSON.stringify(this.headers));
139
+ const payload = base64UrlEncode(JSON.stringify(this.payload));
140
+ const signature = base64UrlEncode(this.signature);
141
+
142
+ return `${headers}.${payload}.${signature}`;
143
+ }
144
+ }
145
+
146
+ export interface TokenProvider {
147
+ getToken(): Token | null;
148
+ }
149
+
150
+ export interface TokenStore extends TokenProvider {
151
+ setToken(token: Token | null): void;
152
+ }
153
+
154
+ export class FixedTokenProvider implements TokenProvider {
155
+ private readonly token: Token | null;
156
+
157
+ public constructor(token: Token | null) {
158
+ this.token = token;
159
+ }
160
+
161
+ public getToken(): Token | null {
162
+ return this.token;
163
+ }
164
+ }
@@ -0,0 +1,460 @@
1
+ import {Logger, NullLogger} from './logging';
2
+ import {Tab, TabEvent, TabUrlChangeEvent, TabVisibilityChangeEvent} from './tab';
3
+ import {OutputChannel} from './channel';
4
+ import {formatCause} from './error';
5
+ import {TokenProvider} from './token';
6
+ import {RetryPolicy} from './retry';
7
+ import {
8
+ Beacon,
9
+ BeaconPayload,
10
+ TrackingEvent,
11
+ TrackingEventContext,
12
+ isCartPartialEvent,
13
+ isIdentifiedUserEvent,
14
+ PartialTrackingEvent,
15
+ } from './trackingEvents';
16
+
17
+ type Options = {
18
+ eventMetadata?: {[key: string]: string},
19
+ };
20
+
21
+ export type Configuration = Options & {
22
+ channel: OutputChannel<Beacon>,
23
+ logger?: Logger,
24
+ tab: Tab,
25
+ tokenProvider: TokenProvider,
26
+ inactivityRetryPolicy: RetryPolicy<number>,
27
+ };
28
+
29
+ type State = {
30
+ initialized: boolean,
31
+ enabled: boolean,
32
+ suspended: boolean,
33
+ };
34
+
35
+ type InactivityTimer = {
36
+ id?: number,
37
+ since: number,
38
+ };
39
+
40
+ export type EventInfo<T extends TrackingEvent = TrackingEvent> = {
41
+ context: TrackingEventContext,
42
+ event: T,
43
+ timestamp: number,
44
+ status: 'pending' | 'confirmed' | 'failed' | 'ignored',
45
+ };
46
+
47
+ export interface EventListener {
48
+ (event: EventInfo): void;
49
+ }
50
+
51
+ const trackedEvents: {[key: string]: {[key: string]: boolean}} = {};
52
+
53
+ export class Tracker {
54
+ private readonly options: Required<Options>;
55
+
56
+ private tab: Tab;
57
+
58
+ private tokenProvider: TokenProvider;
59
+
60
+ private inactivityRetryPolicy: RetryPolicy<any>;
61
+
62
+ private readonly channel: OutputChannel<Beacon>;
63
+
64
+ private readonly logger: Logger;
65
+
66
+ private readonly listeners: EventListener[] = [];
67
+
68
+ private readonly pending: Array<Promise<void>> = [];
69
+
70
+ private readonly state: State = {
71
+ enabled: false,
72
+ initialized: false,
73
+ suspended: false,
74
+ };
75
+
76
+ private readonly inactivityTimer: InactivityTimer = {
77
+ since: 0,
78
+ };
79
+
80
+ public constructor(config: Configuration) {
81
+ const {tab, tokenProvider, channel, logger, inactivityRetryPolicy, ...options} = config;
82
+
83
+ this.tab = tab;
84
+ this.tokenProvider = tokenProvider;
85
+ this.inactivityRetryPolicy = inactivityRetryPolicy;
86
+ this.channel = channel;
87
+ this.logger = logger ?? new NullLogger();
88
+ this.options = {
89
+ ...options,
90
+ eventMetadata: options.eventMetadata ?? {},
91
+ };
92
+
93
+ this.enable = this.enable.bind(this);
94
+ this.disable = this.disable.bind(this);
95
+ this.suspend = this.suspend.bind(this);
96
+ this.unsuspend = this.unsuspend.bind(this);
97
+ this.trackPageLoad = this.trackPageLoad.bind(this);
98
+ this.trackTabVisibilityChange = this.trackTabVisibilityChange.bind(this);
99
+ this.trackTabUrlChange = this.trackTabUrlChange.bind(this);
100
+ this.trackInactivity = this.trackInactivity.bind(this);
101
+ }
102
+
103
+ public addListener(listener: EventListener): void {
104
+ this.listeners.push(listener);
105
+ }
106
+
107
+ public removeListener(listener: EventListener): void {
108
+ let index = this.listeners.indexOf(listener);
109
+
110
+ while (index >= 0) {
111
+ this.listeners.splice(index, 1);
112
+ index = this.listeners.indexOf(listener);
113
+ }
114
+ }
115
+
116
+ public get flushed(): Promise<void> {
117
+ const suppress = (): void => {
118
+ // suppress errors
119
+ };
120
+
121
+ return Promise.all(this.pending).then(suppress, suppress);
122
+ }
123
+
124
+ public isEnabled(): boolean {
125
+ return this.state.enabled;
126
+ }
127
+
128
+ public isSuspended(): boolean {
129
+ return this.state.suspended;
130
+ }
131
+
132
+ public enable(): void {
133
+ if (this.state.enabled) {
134
+ return;
135
+ }
136
+
137
+ this.logger.info('Tracker enabled');
138
+
139
+ this.state.enabled = true;
140
+
141
+ if (this.state.suspended) {
142
+ return;
143
+ }
144
+
145
+ this.startInactivityTimer();
146
+
147
+ if (!this.state.initialized) {
148
+ this.state.initialized = true;
149
+ this.initialize();
150
+ }
151
+
152
+ this.tab.addListener('load', this.trackPageLoad);
153
+ this.tab.addListener('urlChange', this.trackTabUrlChange);
154
+ this.tab.addListener('visibilityChange', this.trackTabVisibilityChange);
155
+ }
156
+
157
+ public disable(): void {
158
+ if (!this.state.enabled) {
159
+ return;
160
+ }
161
+
162
+ this.logger.info('Tracker disabled');
163
+
164
+ this.state.enabled = false;
165
+
166
+ if (this.state.suspended) {
167
+ return;
168
+ }
169
+
170
+ this.tab.removeListener('load', this.trackPageLoad);
171
+ this.tab.removeListener('urlChange', this.trackTabUrlChange);
172
+ this.tab.removeListener('visibilityChange', this.trackTabVisibilityChange);
173
+
174
+ this.stopInactivityTimer();
175
+ }
176
+
177
+ public suspend(): void {
178
+ if (this.state.suspended) {
179
+ return;
180
+ }
181
+
182
+ this.logger.info('Tracker suspended');
183
+
184
+ if (this.state.enabled) {
185
+ this.disable();
186
+ this.state.enabled = true;
187
+ }
188
+
189
+ this.state.suspended = true;
190
+ }
191
+
192
+ public unsuspend(): void {
193
+ if (!this.state.suspended) {
194
+ return;
195
+ }
196
+
197
+ this.logger.info('Tracker unsuspended');
198
+
199
+ this.state.suspended = false;
200
+
201
+ if (this.state.enabled) {
202
+ this.state.enabled = false;
203
+ this.enable();
204
+ }
205
+ }
206
+
207
+ private initialize(): void {
208
+ if (trackedEvents[this.tab.id] === undefined) {
209
+ trackedEvents[this.tab.id] = {};
210
+ }
211
+
212
+ const initEvents = trackedEvents[this.tab.id];
213
+
214
+ if (this.tab.isNew && !initEvents.tabOpened) {
215
+ initEvents.tabOpened = true;
216
+
217
+ this.trackTabOpen({tabId: this.tab.id});
218
+ }
219
+
220
+ if (!initEvents.pageOpened) {
221
+ initEvents.pageOpened = true;
222
+
223
+ this.trackPageOpen({
224
+ url: this.tab.url,
225
+ referrer: this.tab.referrer,
226
+ });
227
+ }
228
+ }
229
+
230
+ private stopInactivityTimer(): void {
231
+ if (this.inactivityTimer.id !== undefined) {
232
+ window.clearTimeout(this.inactivityTimer.id);
233
+
234
+ delete this.inactivityTimer.id;
235
+ }
236
+ }
237
+
238
+ private startInactivityTimer(): void {
239
+ this.stopInactivityTimer();
240
+
241
+ this.inactivityTimer.since = Date.now();
242
+
243
+ let iteration = -1;
244
+
245
+ const startTimer = (): void => {
246
+ if (!this.inactivityRetryPolicy.shouldRetry(iteration + 1, this.inactivityTimer.since)) {
247
+ window.clearTimeout(this.inactivityTimer.id);
248
+
249
+ return;
250
+ }
251
+
252
+ iteration += 1;
253
+
254
+ this.inactivityTimer.id = window.setTimeout(
255
+ () => {
256
+ this.trackInactivity();
257
+ startTimer();
258
+ },
259
+ this.inactivityRetryPolicy.getDelay(iteration),
260
+ );
261
+ };
262
+
263
+ startTimer();
264
+ }
265
+
266
+ public track<T extends PartialTrackingEvent>(event: T, timestamp: number = Date.now()): Promise<T> {
267
+ return this.publish(this.enrichEvent(event, timestamp), timestamp).then(() => event);
268
+ }
269
+
270
+ private trackPageOpen({referrer, ...payload}: {url: string, referrer: string}): void {
271
+ this.enqueue({
272
+ type: 'pageOpened',
273
+ ...payload,
274
+ ...(referrer.length > 0 ? {referrer: referrer} : {}),
275
+ });
276
+ }
277
+
278
+ private trackPageLoad({detail: {tab}}: TabEvent): void {
279
+ this.enqueue({
280
+ type: 'pageLoaded',
281
+ url: tab.url,
282
+ title: tab.title,
283
+ lastModifiedTime: Date.parse(tab.document.lastModified),
284
+ });
285
+ }
286
+
287
+ private trackTabOpen(payload: {tabId: string}): void {
288
+ this.enqueue({
289
+ type: 'tabOpened',
290
+ ...payload,
291
+ });
292
+ }
293
+
294
+ private trackTabUrlChange({detail}: TabUrlChangeEvent): void {
295
+ this.enqueue({
296
+ type: 'tabUrlChanged',
297
+ tabId: detail.tab.id,
298
+ url: detail.url,
299
+ });
300
+ }
301
+
302
+ private trackTabVisibilityChange({detail}: TabVisibilityChangeEvent): void {
303
+ this.enqueue({
304
+ type: 'tabVisibilityChanged',
305
+ tabId: detail.tab.id,
306
+ visibility: detail.visible ? 'visible' : 'hidden',
307
+ });
308
+ }
309
+
310
+ private trackInactivity(): void {
311
+ this.enqueue({
312
+ type: 'nothingChanged',
313
+ sinceTime: this.inactivityTimer.since,
314
+ });
315
+ }
316
+
317
+ private enqueue(event: TrackingEvent, timestamp: number = Date.now()): void {
318
+ this.publish(event, timestamp).catch(() => {
319
+ // suppress error
320
+ });
321
+ }
322
+
323
+ private notifyEvent(event: EventInfo): void {
324
+ this.listeners.map(listener => listener(event));
325
+ }
326
+
327
+ private publish<T extends TrackingEvent>(event: T, timestamp: number): Promise<T> {
328
+ if (event.type !== 'nothingChanged') {
329
+ this.stopInactivityTimer();
330
+ }
331
+
332
+ const metadata = this.options.eventMetadata;
333
+ const context: TrackingEventContext = {
334
+ tabId: this.tab.id,
335
+ url: this.tab.url,
336
+ ...(Object.keys(metadata).length > 0 ? {metadata: metadata} : {}),
337
+ };
338
+
339
+ const eventInfo: EventInfo<T> = {
340
+ event: event,
341
+ context: context,
342
+ timestamp: timestamp,
343
+ status: 'pending',
344
+ };
345
+
346
+ if (this.state.suspended) {
347
+ this.logger.warn(`Tracker is suspended, ignoring event "${event.type}"`);
348
+
349
+ this.notifyEvent({...eventInfo, status: 'ignored'});
350
+
351
+ return Promise.reject(new Error('The tracker is suspended.'));
352
+ }
353
+
354
+ this.logger.info(`Tracked event "${event.type}"`);
355
+
356
+ this.notifyEvent(eventInfo);
357
+
358
+ return new Promise<T>((resolve, reject) => {
359
+ const promise = this.channel
360
+ .publish(this.createBeacon(event, timestamp, context))
361
+ .then(
362
+ () => {
363
+ this.logger.debug(`Successfully published event "${event.type}"`);
364
+
365
+ this.notifyEvent({...eventInfo, status: 'confirmed'});
366
+
367
+ resolve(event);
368
+ },
369
+ cause => {
370
+ this.logger.error(`Failed to publish event "${event.type}", reason: ${formatCause(cause)}`);
371
+
372
+ this.notifyEvent({...eventInfo, status: 'failed'});
373
+
374
+ reject(cause);
375
+ },
376
+ );
377
+
378
+ this.pending.push(promise);
379
+
380
+ promise.finally(() => {
381
+ this.pending.splice(this.pending.indexOf(promise), 1);
382
+ });
383
+
384
+ if (this.state.enabled && event.type !== 'nothingChanged') {
385
+ this.startInactivityTimer();
386
+ }
387
+ });
388
+ }
389
+
390
+ private enrichEvent(event: PartialTrackingEvent, timestamp: number): TrackingEvent {
391
+ if (isCartPartialEvent(event)) {
392
+ const {cart: {lastUpdateTime = timestamp, ...cart}, ...payload} = event;
393
+
394
+ return {
395
+ ...payload,
396
+ cart: {
397
+ ...cart,
398
+ lastUpdateTime: lastUpdateTime,
399
+ },
400
+ };
401
+ }
402
+
403
+ return event;
404
+ }
405
+
406
+ private createBeacon(event: TrackingEvent, timestamp: number, context: TrackingEventContext): Beacon {
407
+ const token = this.tokenProvider.getToken();
408
+
409
+ return {
410
+ timestamp: timestamp,
411
+ ...(token !== null ? {token: token.toString()} : {}),
412
+ context: context,
413
+ payload: this.enrichBeaconPayload(this.createBeaconPayload(event)),
414
+ };
415
+ }
416
+
417
+ private createBeaconPayload(event: TrackingEvent): BeaconPayload {
418
+ if (!isIdentifiedUserEvent(event)) {
419
+ return event;
420
+ }
421
+
422
+ if (event.type === 'userSignedUp' && event.profile !== undefined) {
423
+ const {userId, profile, ...payload} = event;
424
+
425
+ return {
426
+ ...payload,
427
+ externalUserId: userId,
428
+ patch: {
429
+ operations: [
430
+ {
431
+ type: 'set',
432
+ path: '.',
433
+ value: profile,
434
+ },
435
+ ],
436
+ },
437
+ };
438
+ }
439
+
440
+ const {userId, ...payload} = event;
441
+
442
+ return {
443
+ ...payload,
444
+ externalUserId: userId,
445
+ };
446
+ }
447
+
448
+ private enrichBeaconPayload(event: BeaconPayload): BeaconPayload {
449
+ switch (event.type) {
450
+ case 'linkOpened':
451
+ return {
452
+ ...event,
453
+ link: new URL(event.link, this.tab.url).toString(),
454
+ };
455
+
456
+ default:
457
+ return event;
458
+ }
459
+ }
460
+ }