@@ -26,7 +26,9 @@ import {
26
26
27
27
const appliedInjections : Set < string > = new Set ( ) ;
28
28
const appliedMutations : MutationController [ ] = [ ] ;
29
- let previousUrl : string | undefined = undefined ;
29
+ let previousUrl : string | undefined ;
30
+ // Cache to track exposure for the current URL, should be cleared on URL change
31
+ let urlExposureCache : { [ url : string ] : { [ key : string ] : string | undefined } } ;
30
32
31
33
export const initializeExperiment = ( apiKey : string , initialFlags : string ) => {
32
34
const globalScope = getGlobalScope ( ) ;
@@ -37,7 +39,8 @@ export const initializeExperiment = (apiKey: string, initialFlags: string) => {
37
39
if ( ! isLocalStorageAvailable ( ) || ! globalScope ) {
38
40
return ;
39
41
}
40
-
42
+ previousUrl = undefined ;
43
+ urlExposureCache = { } ;
41
44
const experimentStorageName = `EXP_${ apiKey . slice ( 0 , 10 ) } ` ;
42
45
let user : ExperimentUser ;
43
46
try {
@@ -132,6 +135,12 @@ const applyVariants = (variants: Variants | undefined) => {
132
135
if ( ! globalScope ) {
133
136
return ;
134
137
}
138
+ const currentUrl = urlWithoutParamsAndAnchor ( globalScope . location . href ) ;
139
+ // Initialize the cache if on a new URL
140
+ if ( ! urlExposureCache ?. [ currentUrl ] ) {
141
+ urlExposureCache = { } ;
142
+ urlExposureCache [ currentUrl ] = { } ;
143
+ }
135
144
for ( const key in variants ) {
136
145
const variant = variants [ key ] ;
137
146
const isWebExperimentation = variant . metadata ?. deliveryMethod === 'web' ;
@@ -173,18 +182,21 @@ const handleRedirect = (action, key: string, variant: Variant) => {
173
182
const redirectUrl = action ?. data ?. url ;
174
183
175
184
const currentUrl = urlWithoutParamsAndAnchor ( globalScope . location . href ) ;
176
- const shouldTrackExposure =
177
- ( variant . metadata ?. [ 'trackExposure' ] as boolean ) ?? true ;
178
185
179
186
// prevent infinite redirection loop
180
187
if ( currentUrl === referrerUrl ) {
181
188
return ;
182
189
}
190
+
183
191
const targetUrl = concatenateQueryParamsOf (
184
192
globalScope . location . href ,
185
193
redirectUrl ,
186
194
) ;
187
- shouldTrackExposure && globalScope . webExperiment . exposure ( key ) ;
195
+
196
+ exposureWithDedupe ( key , variant ) ;
197
+
198
+ // set previous url - relevant for SPA if redirect happens before push/replaceState is complete
199
+ previousUrl = globalScope . location . href ;
188
200
// perform redirection
189
201
globalScope . location . replace ( targetUrl ) ;
190
202
} ;
@@ -198,9 +210,7 @@ const handleMutate = (action, key: string, variant: Variant) => {
198
210
mutations . forEach ( ( m ) => {
199
211
appliedMutations . push ( mutate . declarative ( m ) ) ;
200
212
} ) ;
201
- const shouldTrackExposure =
202
- ( variant . metadata ?. [ 'trackExposure' ] as boolean ) ?? true ;
203
- shouldTrackExposure && globalScope . webExperiment . exposure ( key ) ;
213
+ exposureWithDedupe ( key , variant ) ;
204
214
} ;
205
215
206
216
const revertMutations = ( ) => {
@@ -279,9 +289,7 @@ const handleInject = (action, key: string, variant: Variant) => {
279
289
appliedInjections . delete ( id ) ;
280
290
} ,
281
291
} ) ;
282
- const shouldTrackExposure =
283
- ( variant . metadata ?. [ 'trackExposure' ] as boolean ) ?? true ;
284
- shouldTrackExposure && globalScope . webExperiment . exposure ( key ) ;
292
+ exposureWithDedupe ( key , variant ) ;
285
293
} ;
286
294
287
295
export const setUrlChangeListener = ( ) => {
@@ -302,25 +310,23 @@ export const setUrlChangeListener = () => {
302
310
303
311
// Wrapper for pushState
304
312
history . pushState = function ( ...args ) {
305
- previousUrl = globalScope . location . href ;
306
313
// Call the original pushState
307
314
const result = originalPushState . apply ( this , args ) ;
308
315
// Revert mutations and apply variants after pushing state
309
316
revertMutations ( ) ;
310
317
applyVariants ( globalScope . webExperiment . all ( ) ) ;
311
-
318
+ previousUrl = globalScope . location . href ;
312
319
return result ;
313
320
} ;
314
321
315
322
// Wrapper for replaceState
316
323
history . replaceState = function ( ...args ) {
317
- previousUrl = globalScope . location . href ;
318
324
// Call the original replaceState
319
325
const result = originalReplaceState . apply ( this , args ) ;
320
- // Revert mutations and apply variants after replacing state
326
+ // Revert mutations and apply variants if the URL has changed
321
327
revertMutations ( ) ;
322
328
applyVariants ( globalScope . webExperiment . all ( ) ) ;
323
-
329
+ previousUrl = globalScope . location . href ;
324
330
return result ;
325
331
} ;
326
332
} ;
@@ -336,3 +342,21 @@ const isPageTargetingSegment = (segment: EvaluationSegment) => {
336
342
segment . metadata ?. segmentName === 'Page is excluded' )
337
343
) ;
338
344
} ;
345
+
346
+ const exposureWithDedupe = ( key : string , variant : Variant ) => {
347
+ const globalScope = getGlobalScope ( ) ;
348
+ if ( ! globalScope ) return ;
349
+
350
+ const shouldTrackVariant = variant . metadata ?. [ 'trackExposure' ] ?? true ;
351
+ const currentUrl = urlWithoutParamsAndAnchor ( globalScope . location . href ) ;
352
+
353
+ // if on the same base URL, only track exposure if variant has changed or has not been tracked
354
+ const hasTrackedVariant =
355
+ urlExposureCache ?. [ currentUrl ] ?. [ key ] === variant . key ;
356
+ const shouldTrackExposure = shouldTrackVariant && ! hasTrackedVariant ;
357
+
358
+ if ( shouldTrackExposure ) {
359
+ globalScope . webExperiment . exposure ( key ) ;
360
+ urlExposureCache [ currentUrl ] [ key ] = variant . key ;
361
+ }
362
+ } ;
0 commit comments