@seekora-ai/search-sdk 0.2.22 → 0.2.24
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/dist/analytics/activeFilters.d.ts +33 -0
- package/dist/analytics/activeFilters.js +145 -0
- package/dist/analytics/events.d.ts +26 -0
- package/dist/analytics/events.js +38 -0
- package/dist/analytics/index.d.ts +6 -0
- package/dist/analytics/index.js +55 -0
- package/dist/analytics/loadAnalytics.d.ts +37 -0
- package/dist/analytics/loadAnalytics.js +70 -0
- package/dist/analytics/searchId.d.ts +26 -0
- package/dist/analytics/searchId.js +102 -0
- package/dist/analytics/track.d.ts +59 -0
- package/dist/analytics/track.js +130 -0
- package/dist/analytics/types.gen.d.ts +781 -0
- package/dist/analytics/types.gen.js +6 -0
- package/dist/client.d.ts +82 -17
- package/dist/client.js +588 -414
- package/dist/index.d.ts +13 -12
- package/dist/index.js +39 -1
- package/package.json +11 -4
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
export declare const SEEKORA_FILTERS_TTL_MS: number;
|
|
2
|
+
export declare const SS_KEY_FILTERS = "seekora.activeFilters";
|
|
3
|
+
export interface ActiveFilter {
|
|
4
|
+
filter_attribute: string;
|
|
5
|
+
filter_value: string;
|
|
6
|
+
at: number;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Record that a filter just became active. Idempotent — toggling the
|
|
10
|
+
* same (attr, value) pair twice in a row only stamps it once (the
|
|
11
|
+
* second call refreshes `at`).
|
|
12
|
+
*/
|
|
13
|
+
export declare function recordActiveFilter(attr: string, value: string, at?: number): void;
|
|
14
|
+
/**
|
|
15
|
+
* Drop a single filter from both layers. Used when the user removes a
|
|
16
|
+
* facet selection via removeRefinement / clearRefinements.
|
|
17
|
+
*/
|
|
18
|
+
export declare function removeActiveFilter(attr: string, value: string): void;
|
|
19
|
+
/**
|
|
20
|
+
* Drop every active filter. Used by clearRefinements and by callers
|
|
21
|
+
* (rare) that want to reset attribution explicitly.
|
|
22
|
+
*/
|
|
23
|
+
export declare function clearActiveFilters(): void;
|
|
24
|
+
/**
|
|
25
|
+
* Return the non-stale active filter set. Reads from both layers and
|
|
26
|
+
* merges so the conversion site doesn't care which layer captured the
|
|
27
|
+
* filter. Stale entries are pruned (and written back to sessionStorage
|
|
28
|
+
* if any were dropped) so a converting user doesn't accumulate a long
|
|
29
|
+
* tail of yesterday's filter selections.
|
|
30
|
+
*
|
|
31
|
+
* `now` is injectable for deterministic testing.
|
|
32
|
+
*/
|
|
33
|
+
export declare function getActiveFilters(now?: number): Array<Pick<ActiveFilter, "filter_attribute" | "filter_value">>;
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// Session-state for the "currently active filter set".
|
|
3
|
+
//
|
|
4
|
+
// Algolia Filter Converted semantics: on every conversion (addToCart /
|
|
5
|
+
// purchase / wishlist / etc.), one Filter Converted event is emitted PER
|
|
6
|
+
// active filter that was applied in the recent window. Funnel queries
|
|
7
|
+
// then answer "conversion rate per filter value" — the only correct way
|
|
8
|
+
// to attribute revenue to faceted browsing.
|
|
9
|
+
//
|
|
10
|
+
// To support that, every Facet Applied tick must leave a breadcrumb the
|
|
11
|
+
// conversion site can read later. This module is that breadcrumb.
|
|
12
|
+
//
|
|
13
|
+
// Mirrors the searchId.ts pattern intentionally:
|
|
14
|
+
// - In-memory layer (fastest, lost on tab close)
|
|
15
|
+
// - sessionStorage layer (survives in-tab navigation; needed because
|
|
16
|
+
// the typical conversion happens on a PDP after the user navigates
|
|
17
|
+
// away from the search/results page)
|
|
18
|
+
// - No URL layer — filter sets are too noisy to URL-stamp; the
|
|
19
|
+
// sessionStorage layer covers the only realistic propagation path.
|
|
20
|
+
//
|
|
21
|
+
// TTL: same 30-min window as search_id, so Filter Converted and Object
|
|
22
|
+
// Converted share an attribution horizon. Each entry tracks its own
|
|
23
|
+
// `at` so stale individual filters get pruned on read even if the
|
|
24
|
+
// session has fresher ones.
|
|
25
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
26
|
+
exports.SS_KEY_FILTERS = exports.SEEKORA_FILTERS_TTL_MS = void 0;
|
|
27
|
+
exports.recordActiveFilter = recordActiveFilter;
|
|
28
|
+
exports.removeActiveFilter = removeActiveFilter;
|
|
29
|
+
exports.clearActiveFilters = clearActiveFilters;
|
|
30
|
+
exports.getActiveFilters = getActiveFilters;
|
|
31
|
+
exports.SEEKORA_FILTERS_TTL_MS = 30 * 60 * 1000;
|
|
32
|
+
exports.SS_KEY_FILTERS = "seekora.activeFilters";
|
|
33
|
+
let inMemoryFilters = [];
|
|
34
|
+
function keyOf(attr, value) {
|
|
35
|
+
return `${attr}::${value}`;
|
|
36
|
+
}
|
|
37
|
+
function loadFromStorage() {
|
|
38
|
+
if (typeof window === "undefined" || !window.sessionStorage)
|
|
39
|
+
return [];
|
|
40
|
+
try {
|
|
41
|
+
const raw = window.sessionStorage.getItem(exports.SS_KEY_FILTERS);
|
|
42
|
+
if (!raw)
|
|
43
|
+
return [];
|
|
44
|
+
const parsed = JSON.parse(raw);
|
|
45
|
+
if (!Array.isArray(parsed))
|
|
46
|
+
return [];
|
|
47
|
+
return parsed.filter((x) => x &&
|
|
48
|
+
typeof x.filter_attribute === "string" &&
|
|
49
|
+
typeof x.filter_value === "string" &&
|
|
50
|
+
typeof x.at === "number");
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
return [];
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
function writeToStorage(filters) {
|
|
57
|
+
if (typeof window === "undefined" || !window.sessionStorage)
|
|
58
|
+
return;
|
|
59
|
+
try {
|
|
60
|
+
window.sessionStorage.setItem(exports.SS_KEY_FILTERS, JSON.stringify(filters));
|
|
61
|
+
}
|
|
62
|
+
catch {
|
|
63
|
+
// Private mode / quota — silently ignore; in-memory still works.
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
function mergeUnique(a, b) {
|
|
67
|
+
const seen = new Map();
|
|
68
|
+
// `a` wins on conflict — caller passes the fresher list first.
|
|
69
|
+
for (const f of a)
|
|
70
|
+
seen.set(keyOf(f.filter_attribute, f.filter_value), f);
|
|
71
|
+
for (const f of b) {
|
|
72
|
+
const k = keyOf(f.filter_attribute, f.filter_value);
|
|
73
|
+
if (!seen.has(k))
|
|
74
|
+
seen.set(k, f);
|
|
75
|
+
}
|
|
76
|
+
return Array.from(seen.values());
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Record that a filter just became active. Idempotent — toggling the
|
|
80
|
+
* same (attr, value) pair twice in a row only stamps it once (the
|
|
81
|
+
* second call refreshes `at`).
|
|
82
|
+
*/
|
|
83
|
+
function recordActiveFilter(attr, value, at = Date.now()) {
|
|
84
|
+
const entry = {
|
|
85
|
+
filter_attribute: attr,
|
|
86
|
+
filter_value: value,
|
|
87
|
+
at,
|
|
88
|
+
};
|
|
89
|
+
const k = keyOf(attr, value);
|
|
90
|
+
inMemoryFilters = [
|
|
91
|
+
entry,
|
|
92
|
+
...inMemoryFilters.filter((f) => keyOf(f.filter_attribute, f.filter_value) !== k),
|
|
93
|
+
];
|
|
94
|
+
// Merge with sessionStorage so we don't clobber filters set in a
|
|
95
|
+
// sibling tab event handler before our in-memory bootstrapped.
|
|
96
|
+
const merged = mergeUnique(inMemoryFilters, loadFromStorage());
|
|
97
|
+
writeToStorage(merged);
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Drop a single filter from both layers. Used when the user removes a
|
|
101
|
+
* facet selection via removeRefinement / clearRefinements.
|
|
102
|
+
*/
|
|
103
|
+
function removeActiveFilter(attr, value) {
|
|
104
|
+
const k = keyOf(attr, value);
|
|
105
|
+
inMemoryFilters = inMemoryFilters.filter((f) => keyOf(f.filter_attribute, f.filter_value) !== k);
|
|
106
|
+
const next = loadFromStorage().filter((f) => keyOf(f.filter_attribute, f.filter_value) !== k);
|
|
107
|
+
writeToStorage(next);
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Drop every active filter. Used by clearRefinements and by callers
|
|
111
|
+
* (rare) that want to reset attribution explicitly.
|
|
112
|
+
*/
|
|
113
|
+
function clearActiveFilters() {
|
|
114
|
+
inMemoryFilters = [];
|
|
115
|
+
if (typeof window !== "undefined" && window.sessionStorage) {
|
|
116
|
+
try {
|
|
117
|
+
window.sessionStorage.removeItem(exports.SS_KEY_FILTERS);
|
|
118
|
+
}
|
|
119
|
+
catch {
|
|
120
|
+
// ignore
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Return the non-stale active filter set. Reads from both layers and
|
|
126
|
+
* merges so the conversion site doesn't care which layer captured the
|
|
127
|
+
* filter. Stale entries are pruned (and written back to sessionStorage
|
|
128
|
+
* if any were dropped) so a converting user doesn't accumulate a long
|
|
129
|
+
* tail of yesterday's filter selections.
|
|
130
|
+
*
|
|
131
|
+
* `now` is injectable for deterministic testing.
|
|
132
|
+
*/
|
|
133
|
+
function getActiveFilters(now = Date.now()) {
|
|
134
|
+
const merged = mergeUnique(inMemoryFilters, loadFromStorage());
|
|
135
|
+
const fresh = merged.filter((f) => now - f.at < exports.SEEKORA_FILTERS_TTL_MS);
|
|
136
|
+
if (fresh.length !== merged.length) {
|
|
137
|
+
// Pruning happened — persist the trimmed list so next read is fast.
|
|
138
|
+
inMemoryFilters = fresh;
|
|
139
|
+
writeToStorage(fresh);
|
|
140
|
+
}
|
|
141
|
+
return fresh.map((f) => ({
|
|
142
|
+
filter_attribute: f.filter_attribute,
|
|
143
|
+
filter_value: f.filter_value,
|
|
144
|
+
}));
|
|
145
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { components } from "./types.gen";
|
|
2
|
+
/** Discriminated union of all 17 V4 analytics events. */
|
|
3
|
+
export type AnalyticsEvent = components["schemas"]["AnalyticsEvent"];
|
|
4
|
+
/** Canonical event-name string constants (spec Section 5A). */
|
|
5
|
+
export declare const EVENT_NAMES: {
|
|
6
|
+
readonly ObjectClicked: "Object Clicked";
|
|
7
|
+
readonly ObjectClickedOutsideSearch: "Object Clicked (Outside Search)";
|
|
8
|
+
readonly FacetApplied: "Facet Applied";
|
|
9
|
+
readonly ObjectConverted: "Object Converted";
|
|
10
|
+
readonly ObjectConvertedOutsideSearch: "Object Converted (Outside Search)";
|
|
11
|
+
readonly FilterConverted: "Filter Converted";
|
|
12
|
+
readonly ProductAdded: "Product Added";
|
|
13
|
+
readonly OrderCompleted: "Order Completed";
|
|
14
|
+
readonly OrderLineCompleted: "Order Line Completed";
|
|
15
|
+
readonly ProductViewed: "Product Viewed";
|
|
16
|
+
readonly ObjectViewed: "Object Viewed";
|
|
17
|
+
readonly FilterViewed: "Filter Viewed";
|
|
18
|
+
readonly SearchSubmitted: "Search Submitted";
|
|
19
|
+
readonly SearchImpression: "Search Impression";
|
|
20
|
+
readonly SearchRefined: "Search Refined";
|
|
21
|
+
readonly SearchEmpty: "Search Empty";
|
|
22
|
+
readonly SuggestionClicked: "Suggestion Clicked";
|
|
23
|
+
};
|
|
24
|
+
export type EventName = (typeof EVENT_NAMES)[keyof typeof EVENT_NAMES];
|
|
25
|
+
/** Flat array of every event-name — handy for whitelist checks and iteration. */
|
|
26
|
+
export declare const ALL_EVENT_NAMES: EventName[];
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// Event-name constants + types for the V4 analytics pipeline.
|
|
3
|
+
//
|
|
4
|
+
// Re-exports the canonical AnalyticsEvent discriminated union from the
|
|
5
|
+
// codegen'd types.gen.ts (which is regenerated from
|
|
6
|
+
// docs/specs/2026-05-15-clickstream-and-search-id-threading.schema.yml via
|
|
7
|
+
// scripts/codegen-events.sh). DO NOT hand-roll types here — every event
|
|
8
|
+
// shape MUST come from the codegen so a schema YAML change is the single
|
|
9
|
+
// source of truth.
|
|
10
|
+
//
|
|
11
|
+
// EVENT_NAMES is the only thing this file owns: the 17 canonical string
|
|
12
|
+
// names that the SDK callers compare/emit against. The drift-pin test
|
|
13
|
+
// (src/analytics/__tests__/schema-drift.test.ts) keeps this list in sync
|
|
14
|
+
// with the YAML discriminator mapping.
|
|
15
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
16
|
+
exports.ALL_EVENT_NAMES = exports.EVENT_NAMES = void 0;
|
|
17
|
+
/** Canonical event-name string constants (spec Section 5A). */
|
|
18
|
+
exports.EVENT_NAMES = {
|
|
19
|
+
ObjectClicked: "Object Clicked",
|
|
20
|
+
ObjectClickedOutsideSearch: "Object Clicked (Outside Search)",
|
|
21
|
+
FacetApplied: "Facet Applied",
|
|
22
|
+
ObjectConverted: "Object Converted",
|
|
23
|
+
ObjectConvertedOutsideSearch: "Object Converted (Outside Search)",
|
|
24
|
+
FilterConverted: "Filter Converted",
|
|
25
|
+
ProductAdded: "Product Added",
|
|
26
|
+
OrderCompleted: "Order Completed",
|
|
27
|
+
OrderLineCompleted: "Order Line Completed",
|
|
28
|
+
ProductViewed: "Product Viewed",
|
|
29
|
+
ObjectViewed: "Object Viewed",
|
|
30
|
+
FilterViewed: "Filter Viewed",
|
|
31
|
+
SearchSubmitted: "Search Submitted",
|
|
32
|
+
SearchImpression: "Search Impression",
|
|
33
|
+
SearchRefined: "Search Refined",
|
|
34
|
+
SearchEmpty: "Search Empty",
|
|
35
|
+
SuggestionClicked: "Suggestion Clicked",
|
|
36
|
+
};
|
|
37
|
+
/** Flat array of every event-name — handy for whitelist checks and iteration. */
|
|
38
|
+
exports.ALL_EVENT_NAMES = Object.values(exports.EVENT_NAMES);
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export { EVENT_NAMES, ALL_EVENT_NAMES, type EventName, type AnalyticsEvent, } from "./events";
|
|
2
|
+
export { SEEKORA_SID_TTL_MS, SS_KEY_SID, SS_KEY_AT, URL_PARAM_SID, URL_PARAM_SIT, setActiveSearchId, resolveSearchId, stampSearchIdOnUrl, } from "./searchId";
|
|
3
|
+
export { SEEKORA_FILTERS_TTL_MS, SS_KEY_FILTERS, recordActiveFilter, removeActiveFilter, clearActiveFilters, getActiveFilters, type ActiveFilter, } from "./activeFilters";
|
|
4
|
+
export { track, trackSearchSubmitted, trackSearchImpression, trackObjectClicked, trackFacetApplied, trackSearchEmpty, trackSearchRefined, trackSuggestionClicked, trackObjectClickedOutsideSearch, trackObjectConverted, trackObjectConvertedOutsideSearch, trackFilterConverted, trackProductAdded, trackOrderCompleted, trackProductViewed, trackObjectViewed, trackFilterViewed, emitFilterConvertedForActiveFilters, type TrackContext, } from "./track";
|
|
5
|
+
export { loadJitsuAnalytics, type LoadJitsuAnalyticsOptions, } from "./loadAnalytics";
|
|
6
|
+
export type { components, paths } from "./types.gen";
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// Public surface of the V4 analytics emission module.
|
|
3
|
+
//
|
|
4
|
+
// Importable as `@seekora-ai/search-sdk/analytics` once the package exports
|
|
5
|
+
// map is updated (see package.json "exports" key — added in a follow-up
|
|
6
|
+
// commit so this module is reachable without deep-importing src/).
|
|
7
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
8
|
+
exports.loadJitsuAnalytics = exports.emitFilterConvertedForActiveFilters = exports.trackFilterViewed = exports.trackObjectViewed = exports.trackProductViewed = exports.trackOrderCompleted = exports.trackProductAdded = exports.trackFilterConverted = exports.trackObjectConvertedOutsideSearch = exports.trackObjectConverted = exports.trackObjectClickedOutsideSearch = exports.trackSuggestionClicked = exports.trackSearchRefined = exports.trackSearchEmpty = exports.trackFacetApplied = exports.trackObjectClicked = exports.trackSearchImpression = exports.trackSearchSubmitted = exports.track = exports.getActiveFilters = exports.clearActiveFilters = exports.removeActiveFilter = exports.recordActiveFilter = exports.SS_KEY_FILTERS = exports.SEEKORA_FILTERS_TTL_MS = exports.stampSearchIdOnUrl = exports.resolveSearchId = exports.setActiveSearchId = exports.URL_PARAM_SIT = exports.URL_PARAM_SID = exports.SS_KEY_AT = exports.SS_KEY_SID = exports.SEEKORA_SID_TTL_MS = exports.ALL_EVENT_NAMES = exports.EVENT_NAMES = void 0;
|
|
9
|
+
var events_1 = require("./events");
|
|
10
|
+
Object.defineProperty(exports, "EVENT_NAMES", { enumerable: true, get: function () { return events_1.EVENT_NAMES; } });
|
|
11
|
+
Object.defineProperty(exports, "ALL_EVENT_NAMES", { enumerable: true, get: function () { return events_1.ALL_EVENT_NAMES; } });
|
|
12
|
+
var searchId_1 = require("./searchId");
|
|
13
|
+
Object.defineProperty(exports, "SEEKORA_SID_TTL_MS", { enumerable: true, get: function () { return searchId_1.SEEKORA_SID_TTL_MS; } });
|
|
14
|
+
Object.defineProperty(exports, "SS_KEY_SID", { enumerable: true, get: function () { return searchId_1.SS_KEY_SID; } });
|
|
15
|
+
Object.defineProperty(exports, "SS_KEY_AT", { enumerable: true, get: function () { return searchId_1.SS_KEY_AT; } });
|
|
16
|
+
Object.defineProperty(exports, "URL_PARAM_SID", { enumerable: true, get: function () { return searchId_1.URL_PARAM_SID; } });
|
|
17
|
+
Object.defineProperty(exports, "URL_PARAM_SIT", { enumerable: true, get: function () { return searchId_1.URL_PARAM_SIT; } });
|
|
18
|
+
Object.defineProperty(exports, "setActiveSearchId", { enumerable: true, get: function () { return searchId_1.setActiveSearchId; } });
|
|
19
|
+
Object.defineProperty(exports, "resolveSearchId", { enumerable: true, get: function () { return searchId_1.resolveSearchId; } });
|
|
20
|
+
Object.defineProperty(exports, "stampSearchIdOnUrl", { enumerable: true, get: function () { return searchId_1.stampSearchIdOnUrl; } });
|
|
21
|
+
// Filter Converted session-state (parallel resolver to searchId).
|
|
22
|
+
// Captures the active filter set so conversion sites can emit one
|
|
23
|
+
// Filter Converted event per active filter — Algolia hybrid pattern.
|
|
24
|
+
var activeFilters_1 = require("./activeFilters");
|
|
25
|
+
Object.defineProperty(exports, "SEEKORA_FILTERS_TTL_MS", { enumerable: true, get: function () { return activeFilters_1.SEEKORA_FILTERS_TTL_MS; } });
|
|
26
|
+
Object.defineProperty(exports, "SS_KEY_FILTERS", { enumerable: true, get: function () { return activeFilters_1.SS_KEY_FILTERS; } });
|
|
27
|
+
Object.defineProperty(exports, "recordActiveFilter", { enumerable: true, get: function () { return activeFilters_1.recordActiveFilter; } });
|
|
28
|
+
Object.defineProperty(exports, "removeActiveFilter", { enumerable: true, get: function () { return activeFilters_1.removeActiveFilter; } });
|
|
29
|
+
Object.defineProperty(exports, "clearActiveFilters", { enumerable: true, get: function () { return activeFilters_1.clearActiveFilters; } });
|
|
30
|
+
Object.defineProperty(exports, "getActiveFilters", { enumerable: true, get: function () { return activeFilters_1.getActiveFilters; } });
|
|
31
|
+
var track_1 = require("./track");
|
|
32
|
+
Object.defineProperty(exports, "track", { enumerable: true, get: function () { return track_1.track; } });
|
|
33
|
+
Object.defineProperty(exports, "trackSearchSubmitted", { enumerable: true, get: function () { return track_1.trackSearchSubmitted; } });
|
|
34
|
+
Object.defineProperty(exports, "trackSearchImpression", { enumerable: true, get: function () { return track_1.trackSearchImpression; } });
|
|
35
|
+
Object.defineProperty(exports, "trackObjectClicked", { enumerable: true, get: function () { return track_1.trackObjectClicked; } });
|
|
36
|
+
Object.defineProperty(exports, "trackFacetApplied", { enumerable: true, get: function () { return track_1.trackFacetApplied; } });
|
|
37
|
+
Object.defineProperty(exports, "trackSearchEmpty", { enumerable: true, get: function () { return track_1.trackSearchEmpty; } });
|
|
38
|
+
Object.defineProperty(exports, "trackSearchRefined", { enumerable: true, get: function () { return track_1.trackSearchRefined; } });
|
|
39
|
+
Object.defineProperty(exports, "trackSuggestionClicked", { enumerable: true, get: function () { return track_1.trackSuggestionClicked; } });
|
|
40
|
+
Object.defineProperty(exports, "trackObjectClickedOutsideSearch", { enumerable: true, get: function () { return track_1.trackObjectClickedOutsideSearch; } });
|
|
41
|
+
Object.defineProperty(exports, "trackObjectConverted", { enumerable: true, get: function () { return track_1.trackObjectConverted; } });
|
|
42
|
+
Object.defineProperty(exports, "trackObjectConvertedOutsideSearch", { enumerable: true, get: function () { return track_1.trackObjectConvertedOutsideSearch; } });
|
|
43
|
+
Object.defineProperty(exports, "trackFilterConverted", { enumerable: true, get: function () { return track_1.trackFilterConverted; } });
|
|
44
|
+
Object.defineProperty(exports, "trackProductAdded", { enumerable: true, get: function () { return track_1.trackProductAdded; } });
|
|
45
|
+
Object.defineProperty(exports, "trackOrderCompleted", { enumerable: true, get: function () { return track_1.trackOrderCompleted; } });
|
|
46
|
+
Object.defineProperty(exports, "trackProductViewed", { enumerable: true, get: function () { return track_1.trackProductViewed; } });
|
|
47
|
+
Object.defineProperty(exports, "trackObjectViewed", { enumerable: true, get: function () { return track_1.trackObjectViewed; } });
|
|
48
|
+
Object.defineProperty(exports, "trackFilterViewed", { enumerable: true, get: function () { return track_1.trackFilterViewed; } });
|
|
49
|
+
Object.defineProperty(exports, "emitFilterConvertedForActiveFilters", { enumerable: true, get: function () { return track_1.emitFilterConvertedForActiveFilters; } });
|
|
50
|
+
// Canonical factory for an analytics-next AnalyticsBrowser pointed at Jitsu.
|
|
51
|
+
// ALWAYS batches -- /v1/t has no CORS, /v1/batch does. Every browser
|
|
52
|
+
// consumer should use this helper instead of hand-rolling AnalyticsBrowser.load().
|
|
53
|
+
// See feedback_analytics_batching_always.md for the rule and rationale.
|
|
54
|
+
var loadAnalytics_1 = require("./loadAnalytics");
|
|
55
|
+
Object.defineProperty(exports, "loadJitsuAnalytics", { enumerable: true, get: function () { return loadAnalytics_1.loadJitsuAnalytics; } });
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { AnalyticsBrowser } from "@segment/analytics-next";
|
|
2
|
+
export interface LoadJitsuAnalyticsOptions {
|
|
3
|
+
/**
|
|
4
|
+
* Per-stream write key (= Jitsu Stream.ID per the SCRUM-263 convention).
|
|
5
|
+
* Provisioned by RegisterJitsuHook at tenant onboarding.
|
|
6
|
+
*/
|
|
7
|
+
writeKey: string;
|
|
8
|
+
/**
|
|
9
|
+
* Full Jitsu Ingest URL with protocol (e.g. "https://jitsu-dev.cptsd.in").
|
|
10
|
+
* The factory strips the protocol + trailing slash and appends "/v1" for
|
|
11
|
+
* `apiHost`, then sets `cdnURL` to the same host. analytics-next's
|
|
12
|
+
* `apiHost` controls where events POST to; `cdnURL` only affects where
|
|
13
|
+
* the library bundle is fetched from -- both matter when redirecting to
|
|
14
|
+
* Jitsu away from Segment defaults.
|
|
15
|
+
*/
|
|
16
|
+
jitsuHost: string;
|
|
17
|
+
/**
|
|
18
|
+
* Per-batch event count threshold. Default 20 -- coalesces rapid bursts
|
|
19
|
+
* (Search Submitted + N per-card Search Impression on the same render
|
|
20
|
+
* tick) without holding too long. Lower this for sandboxed contexts
|
|
21
|
+
* (Shopify Web Pixel) where the page can close during checkout.
|
|
22
|
+
*/
|
|
23
|
+
batchSize?: number;
|
|
24
|
+
/**
|
|
25
|
+
* Max time between flushes, in ms. Default 500 -- short enough for
|
|
26
|
+
* interactive testing, long enough to coalesce. Lower this for short-
|
|
27
|
+
* lived contexts (e.g. set to 100 for Web Pixel on checkout pages
|
|
28
|
+
* where the tab closes immediately after Order Completed).
|
|
29
|
+
*/
|
|
30
|
+
batchTimeoutMs?: number;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Construct an analytics-next AnalyticsBrowser routed at a Jitsu Ingest.
|
|
34
|
+
*
|
|
35
|
+
* Batching is forced ON. See file-level comment for the CORS rationale.
|
|
36
|
+
*/
|
|
37
|
+
export declare function loadJitsuAnalytics(options: LoadJitsuAnalyticsOptions): AnalyticsBrowser;
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// Canonical factory for an analytics-next AnalyticsBrowser instance pointed
|
|
3
|
+
// at a Jitsu Ingest. Centralises the Jitsu-vs-Segment routing knobs so every
|
|
4
|
+
// browser consumer (cartello, demo-stores, ui-sdk built-in widgets, the
|
|
5
|
+
// shopify-app theme JS) inherits the same configuration without copy-pasting.
|
|
6
|
+
//
|
|
7
|
+
// ALWAYS enables batching. Jitsu Ingest exposes CORS
|
|
8
|
+
// (Access-Control-Allow-Origin) on /v1/batch but NOT on /v1/t -- unbatched
|
|
9
|
+
// single events get silently dropped by the browser preflight before they
|
|
10
|
+
// ever leave the page. Verified May 2026 against jitsu-dev.cptsd.in +
|
|
11
|
+
// local-stack. Hard rule: if you emit analytics from a browser, batching is
|
|
12
|
+
// on. No exceptions. See memory/feedback_analytics_batching_always.md.
|
|
13
|
+
//
|
|
14
|
+
// Usage (replaces hand-rolled `AnalyticsBrowser.load()` calls):
|
|
15
|
+
//
|
|
16
|
+
// import { loadJitsuAnalytics } from "@seekora-ai/search-sdk";
|
|
17
|
+
// const analytics = loadJitsuAnalytics({
|
|
18
|
+
// writeKey: SEEKORA_JITSU_WRITE_KEY,
|
|
19
|
+
// jitsuHost: SEEKORA_JITSU_HOST,
|
|
20
|
+
// });
|
|
21
|
+
// <SearchProvider trackContext={{ analytics, orgcode, xstoreid }}>
|
|
22
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
23
|
+
exports.loadJitsuAnalytics = loadJitsuAnalytics;
|
|
24
|
+
const analytics_next_1 = require("@segment/analytics-next");
|
|
25
|
+
/**
|
|
26
|
+
* Construct an analytics-next AnalyticsBrowser routed at a Jitsu Ingest.
|
|
27
|
+
*
|
|
28
|
+
* Batching is forced ON. See file-level comment for the CORS rationale.
|
|
29
|
+
*/
|
|
30
|
+
function loadJitsuAnalytics(options) {
|
|
31
|
+
const apiHost = options.jitsuHost.replace(/^https?:\/\//, "").replace(/\/+$/, "") + "/v1";
|
|
32
|
+
return analytics_next_1.AnalyticsBrowser.load({
|
|
33
|
+
writeKey: options.writeKey,
|
|
34
|
+
cdnURL: options.jitsuHost,
|
|
35
|
+
}, {
|
|
36
|
+
integrations: {
|
|
37
|
+
"Segment.io": {
|
|
38
|
+
apiHost,
|
|
39
|
+
// MANDATORY: batching forces /v1/batch routing where Jitsu has
|
|
40
|
+
// CORS. Unbatched /v1/t is silently CORS-dropped by browsers.
|
|
41
|
+
deliveryStrategy: {
|
|
42
|
+
strategy: "batching",
|
|
43
|
+
config: {
|
|
44
|
+
size: options.batchSize ?? 20,
|
|
45
|
+
timeout: options.batchTimeoutMs ?? 500,
|
|
46
|
+
},
|
|
47
|
+
},
|
|
48
|
+
},
|
|
49
|
+
},
|
|
50
|
+
// Disable analytics-next's internal RemoteMetrics telemetry. It
|
|
51
|
+
// sends `analytics_js.invoke` counters to `${apiHost}/m`; with our
|
|
52
|
+
// apiHost redirected to Jitsu, those POSTs go to /v1/m which
|
|
53
|
+
// Jitsu's ingest doesn't expose (returns 401) -- noise in the
|
|
54
|
+
// console + wasted bandwidth. The metrics config lives on
|
|
55
|
+
// CDNSettings (NOT InitOptions, despite looking similar in the
|
|
56
|
+
// .d.ts -- `metrics?: MetricsOptions` is a CDNSettings field).
|
|
57
|
+
// Override it via updateCDNSettings, which fires right after the
|
|
58
|
+
// settings fetch resolves -- before RemoteMetrics is constructed.
|
|
59
|
+
// sampleRate: 0 short-circuits the flusher entirely.
|
|
60
|
+
updateCDNSettings: (cdnSettings) => {
|
|
61
|
+
return {
|
|
62
|
+
...cdnSettings,
|
|
63
|
+
metrics: {
|
|
64
|
+
...(cdnSettings.metrics ?? {}),
|
|
65
|
+
sampleRate: 0,
|
|
66
|
+
},
|
|
67
|
+
};
|
|
68
|
+
},
|
|
69
|
+
});
|
|
70
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
export declare const SEEKORA_SID_TTL_MS: number;
|
|
2
|
+
export declare const SS_KEY_SID = "seekora.searchId";
|
|
3
|
+
export declare const SS_KEY_AT = "seekora.searchAt";
|
|
4
|
+
export declare const URL_PARAM_SID = "seekora_sid";
|
|
5
|
+
export declare const URL_PARAM_SIT = "seekora_sit";
|
|
6
|
+
/**
|
|
7
|
+
* Record the active search_id at `at` (default = now). Writes to both the
|
|
8
|
+
* in-memory layer and sessionStorage so subsequent reads in this tab — even
|
|
9
|
+
* after navigation — can resolve the same search_id.
|
|
10
|
+
*/
|
|
11
|
+
export declare function setActiveSearchId(id: string, at?: number): void;
|
|
12
|
+
/**
|
|
13
|
+
* Walk the three layers and return the first non-stale search_id, or null.
|
|
14
|
+
* `now` is injectable for deterministic testing.
|
|
15
|
+
*/
|
|
16
|
+
export declare function resolveSearchId(now?: number): string | null;
|
|
17
|
+
/**
|
|
18
|
+
* Stamp `?seekora_sid=<id>&seekora_sit=<at>` onto a destination URL. Used
|
|
19
|
+
* at click time so PDP / cross-tab navigation can recover the originating
|
|
20
|
+
* search_id from the URL even when sessionStorage is unavailable
|
|
21
|
+
* (Shopify Web Pixel sandbox, new browser tab, etc.).
|
|
22
|
+
*
|
|
23
|
+
* Returns a URL string with the two params merged in. Falls back to a
|
|
24
|
+
* manual query-string append if URL parsing fails (rare — defensive).
|
|
25
|
+
*/
|
|
26
|
+
export declare function stampSearchIdOnUrl(url: string, id: string, at?: number): string;
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// Three-layer search_id resolver: in-memory → sessionStorage → URL ?seekora_sid.
|
|
3
|
+
// 30-min TTL guard on every layer. See spec Section 6C / plan Phase 5 Task 5.2.
|
|
4
|
+
//
|
|
5
|
+
// Order of resolution:
|
|
6
|
+
// 1. In-memory (current page state) — set by the SDK when /v1/search returns
|
|
7
|
+
// a search_id, or by the caller via setActiveSearchId().
|
|
8
|
+
// 2. sessionStorage (same-tab persistence) — survives in-tab navigation but
|
|
9
|
+
// not new tabs (intentional — that's what the URL stamp is for).
|
|
10
|
+
// 3. URL params `?seekora_sid=<id>&seekora_sit=<unix_ms>` — set by
|
|
11
|
+
// stampSearchIdOnUrl() on result-card clicks so attribution survives
|
|
12
|
+
// cmd-click new tab / shared link clicks within the TTL window.
|
|
13
|
+
//
|
|
14
|
+
// First non-stale hit wins. If every layer is stale or absent, returns null
|
|
15
|
+
// and the caller emits the event without a search_id (out-of-search context).
|
|
16
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
+
exports.URL_PARAM_SIT = exports.URL_PARAM_SID = exports.SS_KEY_AT = exports.SS_KEY_SID = exports.SEEKORA_SID_TTL_MS = void 0;
|
|
18
|
+
exports.setActiveSearchId = setActiveSearchId;
|
|
19
|
+
exports.resolveSearchId = resolveSearchId;
|
|
20
|
+
exports.stampSearchIdOnUrl = stampSearchIdOnUrl;
|
|
21
|
+
exports.SEEKORA_SID_TTL_MS = 30 * 60 * 1000;
|
|
22
|
+
exports.SS_KEY_SID = "seekora.searchId";
|
|
23
|
+
exports.SS_KEY_AT = "seekora.searchAt";
|
|
24
|
+
exports.URL_PARAM_SID = "seekora_sid";
|
|
25
|
+
exports.URL_PARAM_SIT = "seekora_sit";
|
|
26
|
+
let inMemorySearchId = null;
|
|
27
|
+
let inMemoryAt = 0;
|
|
28
|
+
/**
|
|
29
|
+
* Record the active search_id at `at` (default = now). Writes to both the
|
|
30
|
+
* in-memory layer and sessionStorage so subsequent reads in this tab — even
|
|
31
|
+
* after navigation — can resolve the same search_id.
|
|
32
|
+
*/
|
|
33
|
+
function setActiveSearchId(id, at = Date.now()) {
|
|
34
|
+
inMemorySearchId = id;
|
|
35
|
+
inMemoryAt = at;
|
|
36
|
+
if (typeof window !== "undefined" && window.sessionStorage) {
|
|
37
|
+
try {
|
|
38
|
+
window.sessionStorage.setItem(exports.SS_KEY_SID, id);
|
|
39
|
+
window.sessionStorage.setItem(exports.SS_KEY_AT, String(at));
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
// Private mode / quota — silently ignore; in-memory still works.
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Walk the three layers and return the first non-stale search_id, or null.
|
|
48
|
+
* `now` is injectable for deterministic testing.
|
|
49
|
+
*/
|
|
50
|
+
function resolveSearchId(now = Date.now()) {
|
|
51
|
+
// Layer 1 — in-memory.
|
|
52
|
+
if (inMemorySearchId && now - inMemoryAt < exports.SEEKORA_SID_TTL_MS) {
|
|
53
|
+
return inMemorySearchId;
|
|
54
|
+
}
|
|
55
|
+
// Layer 2 — sessionStorage.
|
|
56
|
+
if (typeof window !== "undefined" && window.sessionStorage) {
|
|
57
|
+
try {
|
|
58
|
+
const stored = window.sessionStorage.getItem(exports.SS_KEY_SID);
|
|
59
|
+
const at = parseInt(window.sessionStorage.getItem(exports.SS_KEY_AT) || "0", 10);
|
|
60
|
+
if (stored && now - at < exports.SEEKORA_SID_TTL_MS)
|
|
61
|
+
return stored;
|
|
62
|
+
}
|
|
63
|
+
catch {
|
|
64
|
+
// ignore
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
// Layer 3 — URL ?seekora_sid + ?seekora_sit.
|
|
68
|
+
if (typeof window !== "undefined" && window.location) {
|
|
69
|
+
try {
|
|
70
|
+
const params = new URLSearchParams(window.location.search);
|
|
71
|
+
const urlSid = params.get(exports.URL_PARAM_SID);
|
|
72
|
+
const urlSit = parseInt(params.get(exports.URL_PARAM_SIT) || "0", 10);
|
|
73
|
+
if (urlSid && now - urlSit < exports.SEEKORA_SID_TTL_MS)
|
|
74
|
+
return urlSid;
|
|
75
|
+
}
|
|
76
|
+
catch {
|
|
77
|
+
// ignore
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
return null;
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Stamp `?seekora_sid=<id>&seekora_sit=<at>` onto a destination URL. Used
|
|
84
|
+
* at click time so PDP / cross-tab navigation can recover the originating
|
|
85
|
+
* search_id from the URL even when sessionStorage is unavailable
|
|
86
|
+
* (Shopify Web Pixel sandbox, new browser tab, etc.).
|
|
87
|
+
*
|
|
88
|
+
* Returns a URL string with the two params merged in. Falls back to a
|
|
89
|
+
* manual query-string append if URL parsing fails (rare — defensive).
|
|
90
|
+
*/
|
|
91
|
+
function stampSearchIdOnUrl(url, id, at = Date.now()) {
|
|
92
|
+
try {
|
|
93
|
+
const u = new URL(url, typeof window !== "undefined" ? window.location.href : "https://x");
|
|
94
|
+
u.searchParams.set(exports.URL_PARAM_SID, id);
|
|
95
|
+
u.searchParams.set(exports.URL_PARAM_SIT, String(at));
|
|
96
|
+
return u.toString();
|
|
97
|
+
}
|
|
98
|
+
catch {
|
|
99
|
+
const sep = url.indexOf("?") === -1 ? "?" : "&";
|
|
100
|
+
return `${url}${sep}${exports.URL_PARAM_SID}=${encodeURIComponent(id)}&${exports.URL_PARAM_SIT}=${at}`;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import type { AnalyticsBrowser } from "@segment/analytics-next";
|
|
2
|
+
import { type EventName } from "./events";
|
|
3
|
+
/**
|
|
4
|
+
* Per-request tracking context. Supplied by the SDK client (or the host
|
|
5
|
+
* app) on every emit so the wrapper knows which @segment/analytics-next
|
|
6
|
+
* instance to talk to and which tenant to stamp.
|
|
7
|
+
*/
|
|
8
|
+
export interface TrackContext {
|
|
9
|
+
analytics: AnalyticsBrowser;
|
|
10
|
+
orgcode: string;
|
|
11
|
+
xstoreid: string;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Emit a track event with canonical Seekora fields auto-stamped.
|
|
15
|
+
*
|
|
16
|
+
* - Tenant context (orgcode, xstoreid) is always merged in.
|
|
17
|
+
* - search_id is auto-resolved via resolveSearchId() when absent. Callers
|
|
18
|
+
* that already know the search_id should pass it explicitly to skip the
|
|
19
|
+
* resolver work.
|
|
20
|
+
*/
|
|
21
|
+
export declare function track(ctx: TrackContext, event: EventName, properties?: Record<string, unknown>): void;
|
|
22
|
+
export declare const trackSearchSubmitted: (ctx: TrackContext, p: Record<string, unknown>) => void;
|
|
23
|
+
export declare const trackSearchImpression: (ctx: TrackContext, p: Record<string, unknown>) => void;
|
|
24
|
+
export declare const trackObjectClicked: (ctx: TrackContext, p: Record<string, unknown>) => void;
|
|
25
|
+
export declare const trackFacetApplied: (ctx: TrackContext, p: Record<string, unknown>) => void;
|
|
26
|
+
export declare const trackSearchEmpty: (ctx: TrackContext, p: Record<string, unknown>) => void;
|
|
27
|
+
export declare const trackSearchRefined: (ctx: TrackContext, p: Record<string, unknown>) => void;
|
|
28
|
+
export declare const trackSuggestionClicked: (ctx: TrackContext, p: Record<string, unknown>) => void;
|
|
29
|
+
export declare const trackObjectClickedOutsideSearch: (ctx: TrackContext, p: Record<string, unknown>) => void;
|
|
30
|
+
export declare const trackObjectConverted: (ctx: TrackContext, p: Record<string, unknown>) => void;
|
|
31
|
+
export declare const trackObjectConvertedOutsideSearch: (ctx: TrackContext, p: Record<string, unknown>) => void;
|
|
32
|
+
export declare const trackFilterConverted: (ctx: TrackContext, p: Record<string, unknown>) => void;
|
|
33
|
+
/**
|
|
34
|
+
* Emit one `Filter Converted` event per currently-active filter.
|
|
35
|
+
*
|
|
36
|
+
* Algolia hybrid pattern: a single conversion (addToCart / purchase /
|
|
37
|
+
* wishlist) gets attributed to EVERY filter that was active in the
|
|
38
|
+
* 30-min attribution window — one event per filter so funnel queries
|
|
39
|
+
* can answer "which filter values converted at what rate".
|
|
40
|
+
*
|
|
41
|
+
* `baseProps` carry the conversion metadata that's identical across
|
|
42
|
+
* all of the emitted events (conversion_type, value, currency, plus
|
|
43
|
+
* any extras the caller wants threaded through). Each emit then
|
|
44
|
+
* stamps the per-filter `filter_attribute` + `filter_value`.
|
|
45
|
+
*
|
|
46
|
+
* If no filters are active (empty session-state OR every filter
|
|
47
|
+
* expired), this is a no-op — the regular Object Converted / Product
|
|
48
|
+
* Added event still goes out (the caller's responsibility), but no
|
|
49
|
+
* Filter Converted gets emitted.
|
|
50
|
+
*
|
|
51
|
+
* Returns the number of events emitted, which is handy for unit tests
|
|
52
|
+
* and for callers that want to log "attributed N filters".
|
|
53
|
+
*/
|
|
54
|
+
export declare function emitFilterConvertedForActiveFilters(ctx: TrackContext, baseProps?: Record<string, unknown>): number;
|
|
55
|
+
export declare const trackProductAdded: (ctx: TrackContext, p: Record<string, unknown>) => void;
|
|
56
|
+
export declare const trackOrderCompleted: (ctx: TrackContext, p: Record<string, unknown>) => void;
|
|
57
|
+
export declare const trackProductViewed: (ctx: TrackContext, p: Record<string, unknown>) => void;
|
|
58
|
+
export declare const trackObjectViewed: (ctx: TrackContext, p: Record<string, unknown>) => void;
|
|
59
|
+
export declare const trackFilterViewed: (ctx: TrackContext, p: Record<string, unknown>) => void;
|