Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
9655c11
fix: use latest beacon from athos packages, improvements
dkonieczek Feb 10, 2026
6a9d4ad
fix: update tracker tests, pr review
dkonieczek Feb 11, 2026
1976b2f
fix: pr feedback
dkonieczek Feb 11, 2026
a32640a
fix: use useCallback as trackingRef to observe ref changes
dkonieczek Feb 11, 2026
b56638d
fix: pr review
dkonieczek Feb 11, 2026
0b982b8
fix: use cssEscape package
dkonieczek Feb 12, 2026
26aea6c
fix: move render event to search method for all controllers
dkonieczek Feb 12, 2026
2738b61
fix: change banner.uid check
dkonieczek Feb 12, 2026
478e2ab
Merge pull request #1441 from searchspring/beaconing-improvements
dkonieczek Feb 12, 2026
b74f8fd
fix(recommendationinstantiator): always run a search on ready the con…
chrisFrazier77 Feb 12, 2026
562d8ad
refactor(recommendationinstantiator): refactor
chrisFrazier77 Feb 12, 2026
ec59ec0
fix(controller): adding support for shadow dom click originations for…
korgon Feb 12, 2026
4563fff
Update packages/snap-controller/src/utils/isClickWithinProductLink.ts
korgon Feb 12, 2026
57005a1
fix: ac filters reset impressions
dkonieczek Feb 12, 2026
1a99359
fix: withtracking improvement
dkonieczek Feb 13, 2026
367f786
fix: impression bug with infinite
dkonieczek Feb 17, 2026
4810e32
fix: remove resetKey
dkonieczek Feb 18, 2026
6e95625
fix: add awaitingReobservationRef
dkonieczek Feb 18, 2026
e5d14c0
Merge pull request #1444 from searchspring/readyController-search
korgon Feb 18, 2026
afd2978
test: refactor composedPath path array
dkonieczek Feb 18, 2026
27a9ecb
refactor: cleanup
dkonieczek Feb 18, 2026
19d3530
Merge pull request #1445 from searchspring/shadow-clicks
korgon Feb 18, 2026
5f2212c
fix: pr review
dkonieczek Feb 19, 2026
83d3d9c
refactor: remove lastSearchKey from class
dkonieczek Feb 19, 2026
ed5e6b1
fix: add resultIdentity to dep arr
dkonieczek Feb 19, 2026
78a5209
Merge remote-tracking branch 'origin/develop' into withTracking-useCa…
dkonieczek Feb 19, 2026
9f884a7
Merge pull request #1446 from searchspring/withTracking-useCallback-alt
dkonieczek Feb 19, 2026
12f76bb
feat(preact/components/withtracking): adjusting the way that impressi…
korgon Feb 19, 2026
fd4241c
Merge pull request #1447 from searchspring/withtracking-feat
korgon Feb 19, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3,023 changes: 2,174 additions & 849 deletions package-lock.json

Large diffs are not rendered by default.

99 changes: 70 additions & 29 deletions packages/snap-controller/src/Autocomplete/AutocompleteController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ import type {
ClickthroughResultsInner,
ClickthroughBannersInner,
BannersInner,
} from '@searchspring/beacon';
} from '@athoscommerce/beacon';
import { CLICK_DUPLICATION_TIMEOUT, isClickWithinProductLink } from '../utils/isClickWithinProductLink';
import { isClickWithinBannerLink } from '../utils/isClickWithinBannerLink';

Expand All @@ -37,6 +37,9 @@ const defaultConfig: AutocompleteControllerConfig = {
selector: '',
action: '',
globals: {},
beacon: {
enabled: true,
},
settings: {
integratedSpellCorrection: false,
initializeFromUrl: true,
Expand Down Expand Up @@ -77,7 +80,6 @@ export class AutocompleteController extends AbstractController {
declare store: AutocompleteStore;
declare config: AutocompleteControllerConfig;
public storage: StorageStore;
private lastSearchQuery: string | undefined;

private events: {
[responseId: string]: {
Expand Down Expand Up @@ -123,16 +125,6 @@ export class AutocompleteController extends AbstractController {
key: `ss-controller-${this.config.id}`,
});

this.eventManager.on('afterStore', async (search: AfterStoreObj, next: Next): Promise<void | boolean> => {
await next();
const controller = search.controller as AutocompleteController;
const responseId = search.response.tracking.responseId;
if (controller.store.loaded && !controller.store.error) {
const data: RenderSchemaData = { responseId };
this.tracker.events.autocomplete.render({ data, siteId: this.config.globals?.siteId });
}
});

// add 'afterSearch' middleware
this.eventManager.on('afterSearch', async (ac: AutocompleteAfterSearchObj, next: Next): Promise<void | boolean> => {
await next();
Expand Down Expand Up @@ -175,6 +167,10 @@ export class AutocompleteController extends AbstractController {
track: AutocompleteTrackMethods = {
banner: {
impression: (_banner): void => {
if (!_banner) {
this.log.warn('No banner provided to track.banner.impression');
return;
}
const { responseId, uid } = _banner;
if (this.events[responseId]?.banner?.[uid]?.impression) {
return;
Expand All @@ -187,11 +183,15 @@ export class AutocompleteController extends AbstractController {
results: [],
};
this.eventManager.fire('track.banner.impression', { controller: this, product: { uid }, trackEvent: data });
this.tracker.events.autocomplete.impression({ data, siteId: this.config.globals?.siteId });
this.config.beacon?.enabled && this.tracker.events.autocomplete.impression({ data, siteId: this.config.globals?.siteId });
this.events[responseId].banner[uid] = this.events[responseId].banner[uid] || {};
this.events[responseId].banner[uid].impression = true;
},
click: (e: MouseEvent, banner: MerchandisingContentBanner): void => {
if (!banner) {
this.log.warn('No banner provided to track.banner.click');
return;
}
const { responseId, uid } = banner;
if (isClickWithinBannerLink(e)) {
if (this.events?.[responseId]?.banner[uid]?.clickThrough) {
Expand All @@ -206,13 +206,17 @@ export class AutocompleteController extends AbstractController {
}
},
clickThrough: (e: MouseEvent, { uid, responseId }): void => {
if (!uid) {
this.log.warn('No banner uid provided to track.banner.clickThrough');
return;
}
const banner: ClickthroughBannersInner = { uid };
const data: ClickthroughSchemaData = {
responseId,
banners: [banner],
};
this.eventManager.fire('track.banner.clickThrough', { controller: this, event: e, product: { uid }, trackEvent: data });
this.tracker.events.autocomplete.clickThrough({ data, siteId: this.config.globals?.siteId });
this.config.beacon?.enabled && this.tracker.events.autocomplete.clickThrough({ data, siteId: this.config.globals?.siteId });
this.events[responseId].banner[uid] = this.events[responseId].banner[uid] || {};
this.events[responseId].banner[uid].clickThrough = true;
setTimeout(() => {
Expand All @@ -222,22 +226,35 @@ export class AutocompleteController extends AbstractController {
},
product: {
clickThrough: (e: MouseEvent, result: Product | Banner): void => {
if (!result) {
this.log.warn('No result provided to track.product.clickThrough');
return;
}
const responseId = result.responseId;
const type = (['product', 'banner'].includes(result.type) ? result.type : 'product') as ResultProductType;
const item: ClickthroughResultsInner = {
type: result.type as ResultProductType,
uid: result.id,
parentId: result.id,
sku: result.mappings.core?.sku,
type,
uid: result.id ? '' + result.id : '',
...(type === 'product'
? {
parentId: result.id ? '' + result.id : '',
sku: result.mappings.core?.sku ? '' + result.mappings.core?.sku : undefined,
}
: {}),
};

const data: ClickthroughSchemaData = {
responseId,
results: [item],
};
this.eventManager.fire('track.product.clickThrough', { controller: this, event: e, product: result, trackEvent: data });
this.tracker.events.autocomplete.clickThrough({ data, siteId: this.config.globals?.siteId });
this.config.beacon?.enabled && this.tracker.events.autocomplete.clickThrough({ data, siteId: this.config.globals?.siteId });
},
click: (e: MouseEvent, result: Product | Banner): void => {
if (!result) {
this.log.warn('No result provided to track.product.click');
return;
}
const responseId = result.responseId;

if (result.type === 'banner' && isClickWithinBannerLink(e)) {
Expand All @@ -263,27 +280,40 @@ export class AutocompleteController extends AbstractController {
}
},
impression: (result: Product | Banner): void => {
if (!result) {
this.log.warn('No result provided to track.product.impression');
return;
}
const responseId = result.responseId;
if (this.events?.[responseId]?.product[result.id]?.impression) {
return;
}
const type = (['product', 'banner'].includes(result.type) ? result.type : 'product') as ResultProductType;
const item: ResultsInner = {
type: result.type as ResultProductType,
uid: result.id,
parentId: result.id,
sku: result.mappings.core?.sku,
type,
uid: result.id ? '' + result.id : '',
...(type === 'product'
? {
parentId: result.id ? '' + result.id : '',
sku: result.mappings.core?.sku ? '' + result.mappings.core?.sku : undefined,
}
: {}),
};
const data: ImpressionSchemaData = {
responseId,
results: [item],
banners: [],
};
this.eventManager.fire('track.product.impression', { controller: this, product: result, trackEvent: data });
this.tracker.events.autocomplete.impression({ data, siteId: this.config.globals?.siteId });
this.config.beacon?.enabled && this.tracker.events.autocomplete.impression({ data, siteId: this.config.globals?.siteId });
this.events[responseId].product[result.id] = this.events[responseId].product[result.id] || {};
this.events[responseId].product[result.id].impression = true;
},
addToCart: (result: Product): void => {
if (!result) {
this.log.warn('No result provided to track.product.addToCart');
return;
}
const responseId = result.responseId;
const product: BeaconProduct = {
parentId: result.id,
Expand All @@ -297,16 +327,20 @@ export class AutocompleteController extends AbstractController {
results: [product],
};
this.eventManager.fire('track.product.addToCart', { controller: this, product: result, trackEvent: data });
this.tracker.events.autocomplete.addToCart({ data, siteId: this.config.globals?.siteId });
this.config.beacon?.enabled && this.tracker.events.autocomplete.addToCart({ data, siteId: this.config.globals?.siteId });
},
},
redirect: ({ redirectURL, responseId }): void => {
if (!redirectURL) {
this.log.warn('No redirectURL provided to track.redirect');
return;
}
const data: RedirectSchemaData = {
responseId,
redirect: redirectURL,
};
this.eventManager.fire('track.redirect', { controller: this, redirectURL, trackEvent: data });
this.tracker.events.autocomplete.redirect({ data, siteId: this.config.globals?.siteId });
this.config.beacon?.enabled && this.tracker.events.autocomplete.redirect({ data, siteId: this.config.globals?.siteId });
},
};

Expand Down Expand Up @@ -774,7 +808,8 @@ export class AutocompleteController extends AbstractController {
const responseId = response.tracking.responseId;
this.events[responseId] = this.events[responseId] || { product: {}, banner: {} };

if (response.search?.query === this.lastSearchQuery) {
const previousResponseId = this.store.results[0]?.responseId;
if (previousResponseId && previousResponseId === responseId) {
const impressedResultIds = Object.keys(this.events[responseId].product || {}).filter(
(resultId) => this.events[responseId].product?.[resultId]?.impression
);
Expand All @@ -787,7 +822,6 @@ export class AutocompleteController extends AbstractController {
};
} else {
this.events[responseId] = { product: {}, banner: {} };
this.lastSearchQuery = response.search?.query;
}

const afterSearchProfile = this.profiler.create({ type: 'event', name: 'afterSearch', context: params }).start();
Expand Down Expand Up @@ -815,6 +849,9 @@ export class AutocompleteController extends AbstractController {
// update the store
this.store.update(response);

const data: RenderSchemaData = { responseId };
this.config.beacon?.enabled && this.tracker.events.autocomplete.render({ data, siteId: this.config.globals?.siteId });

const afterStoreProfile = this.profiler.create({ type: 'event', name: 'afterStore', context: params }).start();

try {
Expand Down Expand Up @@ -884,7 +921,11 @@ export class AutocompleteController extends AbstractController {
};

addToCart = async (_products: Product[] | Product): Promise<void> => {
const products = typeof (_products as Product[]).slice == 'function' ? (_products as Product[]).slice() : [_products];
const products = typeof (_products as Product[])?.slice == 'function' ? (_products as Product[]).slice() : [_products];
if (!_products || products.length === 0) {
this.log.warn('No products provided to autocomplete controller.addToCart');
return;
}
(products as Product[]).forEach((product) => {
this.track.product.addToCart(product);
});
Expand Down
3 changes: 3 additions & 0 deletions packages/snap-controller/src/Finder/FinderController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@ import type { FinderControllerConfig, FinderAfterSearchObj, ControllerServices,

const defaultConfig: FinderControllerConfig = {
id: 'finder',
beacon: {
enabled: true,
},
globals: {
pagination: {
pageSize: 0,
Expand Down
Loading
Loading