@seekora-ai/search-sdk 0.2.7 → 0.2.12
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/client.d.ts +121 -2
- package/dist/client.js +219 -9
- package/dist/generated/api.d.ts +291 -20
- package/dist/generated/api.js +449 -23
- package/dist/index.d.ts +1 -0
- package/dist/index.js +14 -1
- package/dist/ui-components.d.ts +74 -0
- package/dist/ui-components.js +165 -0
- package/package.json +1 -1
- package/dist/src/cdn.d.ts +0 -16
- package/dist/src/cdn.js +0 -26
- package/dist/src/client.d.ts +0 -709
- package/dist/src/client.js +0 -1548
- package/dist/src/config-loader.d.ts +0 -43
- package/dist/src/config-loader.js +0 -147
- package/dist/src/config.d.ts +0 -22
- package/dist/src/config.js +0 -58
- package/dist/src/context-collector.d.ts +0 -273
- package/dist/src/context-collector.js +0 -868
- package/dist/src/event-queue.d.ts +0 -195
- package/dist/src/event-queue.js +0 -424
- package/dist/src/index.d.ts +0 -14
- package/dist/src/index.js +0 -48
- package/dist/src/logger.d.ts +0 -61
- package/dist/src/logger.js +0 -172
- package/dist/src/utils.d.ts +0 -20
- package/dist/src/utils.js +0 -73
package/dist/client.d.ts
CHANGED
|
@@ -66,6 +66,46 @@ export interface SeekoraClientConfig {
|
|
|
66
66
|
time_range?: '7d' | '30d' | '90d';
|
|
67
67
|
[key: string]: unknown;
|
|
68
68
|
};
|
|
69
|
+
/**
|
|
70
|
+
* Automatically track search result impressions
|
|
71
|
+
* When enabled, impression events are sent for all items in search results
|
|
72
|
+
* @default true
|
|
73
|
+
*/
|
|
74
|
+
autoTrackImpressions?: boolean;
|
|
75
|
+
/**
|
|
76
|
+
* Delay before tracking impressions (ms)
|
|
77
|
+
* Useful to ensure items are actually visible to the user
|
|
78
|
+
* @default 0
|
|
79
|
+
*/
|
|
80
|
+
impressionTrackingDelay?: number;
|
|
81
|
+
/**
|
|
82
|
+
* Enable event deduplication (industry standard: Segment, Amplitude pattern)
|
|
83
|
+
* When enabled, generates insert_id for backend deduplication
|
|
84
|
+
*
|
|
85
|
+
* Philosophy: Track everything by default, provide tools for deduplication
|
|
86
|
+
* - false (default): Track all events, no client-side filtering
|
|
87
|
+
* - true: Generate insert_id for optional backend deduplication
|
|
88
|
+
*
|
|
89
|
+
* @default false
|
|
90
|
+
*/
|
|
91
|
+
enableDeduplication?: boolean;
|
|
92
|
+
/**
|
|
93
|
+
* Deduplication time window in milliseconds
|
|
94
|
+
* Events with same user+event_type+item within this window get same insert_id
|
|
95
|
+
*
|
|
96
|
+
* Industry standards:
|
|
97
|
+
* - Segment: 10 minutes (600000ms)
|
|
98
|
+
* - Mixpanel: 5 days (but we use shorter for engagement events)
|
|
99
|
+
* - Amplitude: 7 days (for all events)
|
|
100
|
+
*
|
|
101
|
+
* Recommended by event type:
|
|
102
|
+
* - Engagement (click, view, share): 5 minutes (300000ms)
|
|
103
|
+
* - Conversion (add_to_cart, wishlist): 5 minutes (300000ms)
|
|
104
|
+
* - Purchase: 30 days (2592000000ms) - handled separately
|
|
105
|
+
*
|
|
106
|
+
* @default 300000 (5 minutes)
|
|
107
|
+
*/
|
|
108
|
+
deduplicationWindow?: number;
|
|
69
109
|
}
|
|
70
110
|
/**
|
|
71
111
|
* Search context containing identifiers for linking events to searches
|
|
@@ -78,6 +118,8 @@ export interface SearchContext {
|
|
|
78
118
|
correlationId: string;
|
|
79
119
|
/** Search ID from backend (if present in response) */
|
|
80
120
|
searchId?: string;
|
|
121
|
+
/** Journey ID for session-level tracking */
|
|
122
|
+
journey_id?: string;
|
|
81
123
|
/** User ID if authenticated */
|
|
82
124
|
userId?: string;
|
|
83
125
|
/** Anonymous user ID */
|
|
@@ -101,6 +143,10 @@ export interface ExtendedEventPayload extends DataTypesEventPayload {
|
|
|
101
143
|
correlation_id?: string;
|
|
102
144
|
/** Search ID at top level (also in metadata for backward compat) */
|
|
103
145
|
search_id?: string;
|
|
146
|
+
/** Insert ID for event deduplication (industry standard: Segment/Amplitude pattern) */
|
|
147
|
+
insert_id?: string;
|
|
148
|
+
/** Conversion type for conversion events (add_to_cart, wishlist, purchase, etc.) */
|
|
149
|
+
conversion_type?: string;
|
|
104
150
|
}
|
|
105
151
|
export interface SearchOptions {
|
|
106
152
|
q: string;
|
|
@@ -282,6 +328,8 @@ export declare class SeekoraClient {
|
|
|
282
328
|
private enableEventQueue;
|
|
283
329
|
private eventQueue;
|
|
284
330
|
private clientConfig;
|
|
331
|
+
private enableDeduplication;
|
|
332
|
+
private deduplicationWindow;
|
|
285
333
|
constructor(config?: SeekoraClientConfig);
|
|
286
334
|
/**
|
|
287
335
|
* Search for documents
|
|
@@ -297,11 +345,17 @@ export declare class SeekoraClient {
|
|
|
297
345
|
/**
|
|
298
346
|
* Get query suggestions
|
|
299
347
|
*
|
|
300
|
-
*
|
|
301
|
-
*
|
|
348
|
+
* **NEW:** Full GET/POST parity! Automatically chooses the best method:
|
|
349
|
+
* - GET: Default for simple requests, including filtered_tabs with ≤5 tabs (faster, cacheable)
|
|
350
|
+
* - POST: Used for complex filtered_tabs (>5 tabs) or when explicitly requested via options.method = 'POST'
|
|
351
|
+
*
|
|
352
|
+
* With returnFullResponse: true returns full shape including extensions (dropdown_recommendations with trending products, filtered_tabs).
|
|
302
353
|
*
|
|
303
354
|
* @param query - Partial query to get suggestions for
|
|
304
355
|
* @param options - Optional parameters for suggestions
|
|
356
|
+
* @param options.filtered_tabs - Tab configurations (now supported in GET!)
|
|
357
|
+
* @param options.include_dropdown_recommendations - Include rich dropdown data
|
|
358
|
+
* @param options.method - Explicitly set 'POST' to force POST method
|
|
305
359
|
* @returns Suggestion hits array, or QuerySuggestionsFullResponse when returnFullResponse is true
|
|
306
360
|
*/
|
|
307
361
|
getSuggestions(query: string, options?: {
|
|
@@ -473,6 +527,11 @@ export declare class SeekoraClient {
|
|
|
473
527
|
* Send a batch of events directly to the backend (bypasses queue)
|
|
474
528
|
*/
|
|
475
529
|
private sendEventsBatchDirect;
|
|
530
|
+
/**
|
|
531
|
+
* Automatically track search result impressions
|
|
532
|
+
* Creates a batch of view events for all items in search results
|
|
533
|
+
*/
|
|
534
|
+
private autoTrackSearchImpressions;
|
|
476
535
|
/**
|
|
477
536
|
* Get browser context (sync if cached, otherwise returns null)
|
|
478
537
|
* Use collectBrowserContext() for async collection
|
|
@@ -482,6 +541,19 @@ export declare class SeekoraClient {
|
|
|
482
541
|
* Collect browser context asynchronously
|
|
483
542
|
*/
|
|
484
543
|
collectBrowserContext(): Promise<BrowserContext>;
|
|
544
|
+
/**
|
|
545
|
+
* Generate insert_id for event deduplication (industry standard)
|
|
546
|
+
*
|
|
547
|
+
* Format: {user_key}_{event_type}_{item_id}_{timestamp_window}
|
|
548
|
+
* Similar to Segment, Amplitude, Mixpanel deduplication keys
|
|
549
|
+
*
|
|
550
|
+
* @param eventType - Event name (search, click, conversion, etc.)
|
|
551
|
+
* @param itemId - Item ID (optional, for item-specific events)
|
|
552
|
+
* @param userId - User ID or anonymous ID
|
|
553
|
+
* @param window - Deduplication window in milliseconds
|
|
554
|
+
* @returns insert_id string for backend deduplication
|
|
555
|
+
*/
|
|
556
|
+
private generateInsertId;
|
|
485
557
|
/**
|
|
486
558
|
* Build event payload with identifiers and browser context
|
|
487
559
|
* Ensures user_id or anon_id is present, and sets correlation_id/search_id at top level
|
|
@@ -586,6 +658,53 @@ export declare class SeekoraClient {
|
|
|
586
658
|
* @param context - Optional search context for linking events to searches
|
|
587
659
|
*/
|
|
588
660
|
trackCustom(eventName: string, payload?: Partial<DataTypesEventPayload>, context?: SearchContext): Promise<void>;
|
|
661
|
+
/**
|
|
662
|
+
* Track add to cart conversion event
|
|
663
|
+
*
|
|
664
|
+
* @param params - Add to cart parameters
|
|
665
|
+
* @param params.itemId - Item ID that was added to cart
|
|
666
|
+
* @param params.quantity - Quantity added (default: 1)
|
|
667
|
+
* @param params.value - Item value/price
|
|
668
|
+
* @param params.currency - Currency code (default: 'USD')
|
|
669
|
+
* @param params.position - Position in search results (if from search)
|
|
670
|
+
* @param params.searchContext - Search context for attribution
|
|
671
|
+
*/
|
|
672
|
+
trackAddToCart(params: {
|
|
673
|
+
itemId: string;
|
|
674
|
+
quantity?: number;
|
|
675
|
+
value?: number;
|
|
676
|
+
currency?: string;
|
|
677
|
+
position?: number;
|
|
678
|
+
searchContext?: SearchContext;
|
|
679
|
+
}): Promise<void>;
|
|
680
|
+
/**
|
|
681
|
+
* Track add to wishlist conversion event
|
|
682
|
+
*
|
|
683
|
+
* @param params - Add to wishlist parameters
|
|
684
|
+
* @param params.itemId - Item ID that was added to wishlist
|
|
685
|
+
* @param params.position - Position in search results (if from search)
|
|
686
|
+
* @param params.searchContext - Search context for attribution
|
|
687
|
+
*/
|
|
688
|
+
trackAddToWishlist(params: {
|
|
689
|
+
itemId: string;
|
|
690
|
+
position?: number;
|
|
691
|
+
searchContext?: SearchContext;
|
|
692
|
+
}): Promise<void>;
|
|
693
|
+
/**
|
|
694
|
+
* Track item share event
|
|
695
|
+
*
|
|
696
|
+
* @param params - Share parameters
|
|
697
|
+
* @param params.itemId - Item ID that was shared
|
|
698
|
+
* @param params.shareMethod - Share method (e.g., 'facebook', 'twitter', 'whatsapp', 'copy_link')
|
|
699
|
+
* @param params.position - Position in search results (if from search)
|
|
700
|
+
* @param params.searchContext - Search context for attribution
|
|
701
|
+
*/
|
|
702
|
+
trackShare(params: {
|
|
703
|
+
itemId: string;
|
|
704
|
+
shareMethod?: string;
|
|
705
|
+
position?: number;
|
|
706
|
+
searchContext?: SearchContext;
|
|
707
|
+
}): Promise<void>;
|
|
589
708
|
/**
|
|
590
709
|
* Validate an event payload before sending
|
|
591
710
|
* Useful for debugging and ensuring events are properly formatted
|
package/dist/client.js
CHANGED
|
@@ -56,6 +56,9 @@ class SeekoraClient {
|
|
|
56
56
|
this.eventQueue.setLogger(this.logger);
|
|
57
57
|
this.eventQueue.setSender(this.createQueueSender());
|
|
58
58
|
}
|
|
59
|
+
// Initialize deduplication settings (industry standard: Segment/Amplitude pattern)
|
|
60
|
+
this.enableDeduplication = config.enableDeduplication || false;
|
|
61
|
+
this.deduplicationWindow = config.deduplicationWindow || 300000; // Default: 5 minutes
|
|
59
62
|
// Log identifier initialization
|
|
60
63
|
this.logger.verbose('Client identifiers initialized', {
|
|
61
64
|
hasUserId: !!this.userId,
|
|
@@ -63,7 +66,9 @@ class SeekoraClient {
|
|
|
63
66
|
sessionId: this.sessionId.substring(0, 8) + '...',
|
|
64
67
|
autoTrackSearch: this.autoTrackSearch,
|
|
65
68
|
enableContextCollection: this.enableContextCollection,
|
|
66
|
-
enableEventQueue: this.enableEventQueue
|
|
69
|
+
enableEventQueue: this.enableEventQueue,
|
|
70
|
+
enableDeduplication: this.enableDeduplication,
|
|
71
|
+
deduplicationWindow: this.deduplicationWindow
|
|
67
72
|
});
|
|
68
73
|
this.logger.verbose('Initializing SeekoraClient', {
|
|
69
74
|
storeId: this.storeId,
|
|
@@ -275,6 +280,22 @@ class SeekoraClient {
|
|
|
275
280
|
this.logger.warn('Failed to auto-track search event', { error: err.message });
|
|
276
281
|
});
|
|
277
282
|
}
|
|
283
|
+
// Automatically track search result impressions if enabled
|
|
284
|
+
if (this.clientConfig.autoTrackImpressions !== false && results.results && results.results.length > 0) {
|
|
285
|
+
const delay = this.clientConfig.impressionTrackingDelay || 0;
|
|
286
|
+
const trackImpressions = () => {
|
|
287
|
+
this.autoTrackSearchImpressions(results, context).catch((err) => {
|
|
288
|
+
// Log but don't fail the search if tracking fails
|
|
289
|
+
this.logger.warn('Failed to auto-track impressions', { error: err.message });
|
|
290
|
+
});
|
|
291
|
+
};
|
|
292
|
+
if (delay > 0) {
|
|
293
|
+
setTimeout(trackImpressions, delay);
|
|
294
|
+
}
|
|
295
|
+
else {
|
|
296
|
+
trackImpressions();
|
|
297
|
+
}
|
|
298
|
+
}
|
|
278
299
|
return results;
|
|
279
300
|
}
|
|
280
301
|
catch (error) {
|
|
@@ -304,11 +325,17 @@ class SeekoraClient {
|
|
|
304
325
|
/**
|
|
305
326
|
* Get query suggestions
|
|
306
327
|
*
|
|
307
|
-
*
|
|
308
|
-
*
|
|
328
|
+
* **NEW:** Full GET/POST parity! Automatically chooses the best method:
|
|
329
|
+
* - GET: Default for simple requests, including filtered_tabs with ≤5 tabs (faster, cacheable)
|
|
330
|
+
* - POST: Used for complex filtered_tabs (>5 tabs) or when explicitly requested via options.method = 'POST'
|
|
331
|
+
*
|
|
332
|
+
* With returnFullResponse: true returns full shape including extensions (dropdown_recommendations with trending products, filtered_tabs).
|
|
309
333
|
*
|
|
310
334
|
* @param query - Partial query to get suggestions for
|
|
311
335
|
* @param options - Optional parameters for suggestions
|
|
336
|
+
* @param options.filtered_tabs - Tab configurations (now supported in GET!)
|
|
337
|
+
* @param options.include_dropdown_recommendations - Include rich dropdown data
|
|
338
|
+
* @param options.method - Explicitly set 'POST' to force POST method
|
|
312
339
|
* @returns Suggestion hits array, or QuerySuggestionsFullResponse when returnFullResponse is true
|
|
313
340
|
*/
|
|
314
341
|
async getSuggestions(query, options) {
|
|
@@ -324,10 +351,14 @@ class SeekoraClient {
|
|
|
324
351
|
headers['x-anon-id'] = this.anonId;
|
|
325
352
|
if (this.sessionId)
|
|
326
353
|
headers['x-session-id'] = this.sessionId;
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
354
|
+
// Use POST for:
|
|
355
|
+
// - Complex filtered_tabs (5+ tabs or very long JSON would exceed URL limits)
|
|
356
|
+
// - Explicit preference for POST (options.method === 'POST')
|
|
357
|
+
// Use GET for everything else (including simple filtered_tabs)
|
|
358
|
+
const hasComplexFilteredTabs = options?.filtered_tabs &&
|
|
359
|
+
options.filtered_tabs.length > 5;
|
|
360
|
+
const usePost = hasComplexFilteredTabs ||
|
|
361
|
+
options?.method === 'POST'; // Allow explicit POST preference
|
|
331
362
|
const defaults = this.clientConfig?.suggestionsDefaults;
|
|
332
363
|
const buildRequestBody = () => {
|
|
333
364
|
const body = {
|
|
@@ -385,15 +416,27 @@ class SeekoraClient {
|
|
|
385
416
|
});
|
|
386
417
|
return suggestions;
|
|
387
418
|
}
|
|
388
|
-
// GET for simple requests
|
|
419
|
+
// GET for simple requests and simple filtered_tabs (URL-encoded JSON)
|
|
389
420
|
this.logger.verbose('Using GET endpoint', {
|
|
390
421
|
endpoint: '/api/v1/suggestions/queries',
|
|
391
422
|
query,
|
|
423
|
+
hasFilteredTabs: !!(options?.filtered_tabs && options.filtered_tabs.length > 0),
|
|
392
424
|
});
|
|
393
425
|
const analyticsTags = Array.isArray(options?.analytics_tags)
|
|
394
426
|
? options.analytics_tags.join(',')
|
|
395
427
|
: options?.analytics_tags;
|
|
396
|
-
|
|
428
|
+
// Encode filtered_tabs as JSON string for URL parameter
|
|
429
|
+
const filteredTabsParam = options?.filtered_tabs
|
|
430
|
+
? JSON.stringify(options.filtered_tabs)
|
|
431
|
+
: undefined;
|
|
432
|
+
const response = await this.suggestionsApi.v1SuggestionsQueriesGet(this.storeId, this.readSecret, this.userId, this.anonId, this.sessionId, query, // query parameter
|
|
433
|
+
undefined, // q parameter (alias for query)
|
|
434
|
+
options?.hitsPerPage || 5, options?.page, analyticsTags, options?.tags_match_mode, options?.include_categories, options?.include_facets, options?.include_dropdown_recommendations, options?.include_dropdown_product_list, options?.include_filtered_tabs, undefined, // include_empty_query_recommendations
|
|
435
|
+
options?.max_categories, options?.max_facets, options?.min_popularity, options?.time_range, options?.disable_typo_tolerance, filteredTabsParam, // filtered_tabs as JSON string
|
|
436
|
+
undefined, // userId (using header instead)
|
|
437
|
+
undefined, // anonId (using header instead)
|
|
438
|
+
undefined, // sessionId (using header instead)
|
|
439
|
+
{ headers });
|
|
397
440
|
const responseData = response.data?.data || response.data;
|
|
398
441
|
const suggestions = responseData?.results?.[0]?.hits || responseData?.hits || [];
|
|
399
442
|
if (options?.returnFullResponse) {
|
|
@@ -1026,6 +1069,52 @@ class SeekoraClient {
|
|
|
1026
1069
|
throw new Error(`Failed to send batch events: ${response.status}`);
|
|
1027
1070
|
}
|
|
1028
1071
|
}
|
|
1072
|
+
/**
|
|
1073
|
+
* Automatically track search result impressions
|
|
1074
|
+
* Creates a batch of view events for all items in search results
|
|
1075
|
+
*/
|
|
1076
|
+
async autoTrackSearchImpressions(searchResponse, context) {
|
|
1077
|
+
const items = searchResponse.results || [];
|
|
1078
|
+
if (items.length === 0) {
|
|
1079
|
+
return;
|
|
1080
|
+
}
|
|
1081
|
+
this.logger.verbose('Auto-tracking search impressions', {
|
|
1082
|
+
itemCount: items.length,
|
|
1083
|
+
query: searchResponse.query || searchResponse.data?.data?.query,
|
|
1084
|
+
searchId: context.searchId
|
|
1085
|
+
});
|
|
1086
|
+
// Create batch impression events
|
|
1087
|
+
const impressionEvents = items.map((item, index) => ({
|
|
1088
|
+
event_name: 'view',
|
|
1089
|
+
event_id: (0, utils_1.generateUUID)(),
|
|
1090
|
+
event_ts: new Date().toISOString(),
|
|
1091
|
+
// Link to search
|
|
1092
|
+
search_id: context.searchId,
|
|
1093
|
+
journey_id: context.journey_id,
|
|
1094
|
+
correlation_id: context.correlationId,
|
|
1095
|
+
session_id: context.sessionId || this.sessionId,
|
|
1096
|
+
// User context
|
|
1097
|
+
user_id: context.userId || this.userId,
|
|
1098
|
+
anonymous_id: context.anonId || this.anonId,
|
|
1099
|
+
// Item details
|
|
1100
|
+
clicked_item_id: item.id,
|
|
1101
|
+
position: index + 1,
|
|
1102
|
+
query: searchResponse.query || searchResponse.data?.data?.query,
|
|
1103
|
+
// Optional metadata
|
|
1104
|
+
analytics_tags: ['source:search', 'auto_tracked:true']
|
|
1105
|
+
}));
|
|
1106
|
+
try {
|
|
1107
|
+
await this.trackEvents(impressionEvents);
|
|
1108
|
+
this.logger.debug('Auto-tracked search impressions', { count: impressionEvents.length });
|
|
1109
|
+
}
|
|
1110
|
+
catch (error) {
|
|
1111
|
+
this.logger.error('Failed to auto-track impressions', {
|
|
1112
|
+
error: error.message,
|
|
1113
|
+
itemCount: items.length
|
|
1114
|
+
});
|
|
1115
|
+
throw error;
|
|
1116
|
+
}
|
|
1117
|
+
}
|
|
1029
1118
|
/**
|
|
1030
1119
|
* Get browser context (sync if cached, otherwise returns null)
|
|
1031
1120
|
* Use collectBrowserContext() for async collection
|
|
@@ -1040,6 +1129,31 @@ class SeekoraClient {
|
|
|
1040
1129
|
this.cachedBrowserContext = await this.contextCollector.collect();
|
|
1041
1130
|
return this.cachedBrowserContext;
|
|
1042
1131
|
}
|
|
1132
|
+
/**
|
|
1133
|
+
* Generate insert_id for event deduplication (industry standard)
|
|
1134
|
+
*
|
|
1135
|
+
* Format: {user_key}_{event_type}_{item_id}_{timestamp_window}
|
|
1136
|
+
* Similar to Segment, Amplitude, Mixpanel deduplication keys
|
|
1137
|
+
*
|
|
1138
|
+
* @param eventType - Event name (search, click, conversion, etc.)
|
|
1139
|
+
* @param itemId - Item ID (optional, for item-specific events)
|
|
1140
|
+
* @param userId - User ID or anonymous ID
|
|
1141
|
+
* @param window - Deduplication window in milliseconds
|
|
1142
|
+
* @returns insert_id string for backend deduplication
|
|
1143
|
+
*/
|
|
1144
|
+
generateInsertId(eventType, itemId, userId, window) {
|
|
1145
|
+
// Round timestamp to nearest window (5 min default)
|
|
1146
|
+
const timestamp = Math.floor(Date.now() / window);
|
|
1147
|
+
// Format: userKey_eventType_itemId_timestampWindow
|
|
1148
|
+
// Item ID is optional (not all events have items)
|
|
1149
|
+
const parts = [
|
|
1150
|
+
userId.replace(/[^a-zA-Z0-9]/g, '_'), // Sanitize user ID
|
|
1151
|
+
eventType.replace(/[^a-zA-Z0-9]/g, '_'), // Sanitize event type
|
|
1152
|
+
itemId ? itemId.replace(/[^a-zA-Z0-9]/g, '_') : 'null',
|
|
1153
|
+
timestamp.toString()
|
|
1154
|
+
];
|
|
1155
|
+
return parts.join('_');
|
|
1156
|
+
}
|
|
1043
1157
|
/**
|
|
1044
1158
|
* Build event payload with identifiers and browser context
|
|
1045
1159
|
* Ensures user_id or anon_id is present, and sets correlation_id/search_id at top level
|
|
@@ -1121,6 +1235,26 @@ class SeekoraClient {
|
|
|
1121
1235
|
payload.is_tablet = ctx.is_tablet ? 1 : 0;
|
|
1122
1236
|
payload.is_touch_device = ctx.is_touch_device;
|
|
1123
1237
|
}
|
|
1238
|
+
// Generate insert_id for deduplication (industry standard: Segment/Amplitude pattern)
|
|
1239
|
+
// Always enable for purchase events (revenue must be accurate)
|
|
1240
|
+
// Optional for other events based on config
|
|
1241
|
+
const isPurchaseEvent = payload.event_name === 'conversion' &&
|
|
1242
|
+
(payload.conversion_type === 'purchase' || payload.conversion_type === 'checkout_completed');
|
|
1243
|
+
if (this.enableDeduplication || isPurchaseEvent) {
|
|
1244
|
+
const userKey = payload.user_id || payload.anon_id || this.anonId;
|
|
1245
|
+
const eventType = payload.event_name || 'unknown';
|
|
1246
|
+
const itemId = payload.clicked_item_id;
|
|
1247
|
+
// Use longer window for purchase events (30 days)
|
|
1248
|
+
const window = isPurchaseEvent ? 2592000000 : this.deduplicationWindow;
|
|
1249
|
+
payload.insert_id = this.generateInsertId(eventType, itemId, userKey, window);
|
|
1250
|
+
this.logger.verbose('Generated insert_id for deduplication', {
|
|
1251
|
+
event_name: eventType,
|
|
1252
|
+
item_id: itemId,
|
|
1253
|
+
insert_id: payload.insert_id,
|
|
1254
|
+
is_purchase: isPurchaseEvent,
|
|
1255
|
+
window_ms: window
|
|
1256
|
+
});
|
|
1257
|
+
}
|
|
1124
1258
|
return payload;
|
|
1125
1259
|
}
|
|
1126
1260
|
/**
|
|
@@ -1327,6 +1461,82 @@ class SeekoraClient {
|
|
|
1327
1461
|
event_name: eventName,
|
|
1328
1462
|
}, context);
|
|
1329
1463
|
}
|
|
1464
|
+
/**
|
|
1465
|
+
* Track add to cart conversion event
|
|
1466
|
+
*
|
|
1467
|
+
* @param params - Add to cart parameters
|
|
1468
|
+
* @param params.itemId - Item ID that was added to cart
|
|
1469
|
+
* @param params.quantity - Quantity added (default: 1)
|
|
1470
|
+
* @param params.value - Item value/price
|
|
1471
|
+
* @param params.currency - Currency code (default: 'USD')
|
|
1472
|
+
* @param params.position - Position in search results (if from search)
|
|
1473
|
+
* @param params.searchContext - Search context for attribution
|
|
1474
|
+
*/
|
|
1475
|
+
async trackAddToCart(params) {
|
|
1476
|
+
this.logger.verbose('Tracking add to cart event', {
|
|
1477
|
+
itemId: params.itemId,
|
|
1478
|
+
quantity: params.quantity,
|
|
1479
|
+
value: params.value
|
|
1480
|
+
});
|
|
1481
|
+
await this.trackEvent({
|
|
1482
|
+
event_name: 'conversion',
|
|
1483
|
+
conversion_type: 'add_to_cart',
|
|
1484
|
+
clicked_item_id: params.itemId,
|
|
1485
|
+
quantity: params.quantity || 1,
|
|
1486
|
+
value: params.value,
|
|
1487
|
+
currency: params.currency || 'USD',
|
|
1488
|
+
position: params.position,
|
|
1489
|
+
analytics_tags: ['action:add_to_cart'],
|
|
1490
|
+
}, params.searchContext);
|
|
1491
|
+
}
|
|
1492
|
+
/**
|
|
1493
|
+
* Track add to wishlist conversion event
|
|
1494
|
+
*
|
|
1495
|
+
* @param params - Add to wishlist parameters
|
|
1496
|
+
* @param params.itemId - Item ID that was added to wishlist
|
|
1497
|
+
* @param params.position - Position in search results (if from search)
|
|
1498
|
+
* @param params.searchContext - Search context for attribution
|
|
1499
|
+
*/
|
|
1500
|
+
async trackAddToWishlist(params) {
|
|
1501
|
+
this.logger.verbose('Tracking add to wishlist event', {
|
|
1502
|
+
itemId: params.itemId
|
|
1503
|
+
});
|
|
1504
|
+
await this.trackEvent({
|
|
1505
|
+
event_name: 'conversion',
|
|
1506
|
+
conversion_type: 'wishlist',
|
|
1507
|
+
clicked_item_id: params.itemId,
|
|
1508
|
+
position: params.position,
|
|
1509
|
+
analytics_tags: ['action:add_to_wishlist'],
|
|
1510
|
+
}, params.searchContext);
|
|
1511
|
+
}
|
|
1512
|
+
/**
|
|
1513
|
+
* Track item share event
|
|
1514
|
+
*
|
|
1515
|
+
* @param params - Share parameters
|
|
1516
|
+
* @param params.itemId - Item ID that was shared
|
|
1517
|
+
* @param params.shareMethod - Share method (e.g., 'facebook', 'twitter', 'whatsapp', 'copy_link')
|
|
1518
|
+
* @param params.position - Position in search results (if from search)
|
|
1519
|
+
* @param params.searchContext - Search context for attribution
|
|
1520
|
+
*/
|
|
1521
|
+
async trackShare(params) {
|
|
1522
|
+
this.logger.verbose('Tracking share event', {
|
|
1523
|
+
itemId: params.itemId,
|
|
1524
|
+
shareMethod: params.shareMethod
|
|
1525
|
+
});
|
|
1526
|
+
await this.trackEvent({
|
|
1527
|
+
event_name: 'custom',
|
|
1528
|
+
clicked_item_id: params.itemId,
|
|
1529
|
+
position: params.position,
|
|
1530
|
+
analytics_tags: [
|
|
1531
|
+
'action:share',
|
|
1532
|
+
`share_method:${params.shareMethod || 'unknown'}`
|
|
1533
|
+
],
|
|
1534
|
+
custom_json: JSON.stringify({
|
|
1535
|
+
share_method: params.shareMethod,
|
|
1536
|
+
action_type: 'share'
|
|
1537
|
+
}),
|
|
1538
|
+
}, params.searchContext);
|
|
1539
|
+
}
|
|
1330
1540
|
/**
|
|
1331
1541
|
* Validate an event payload before sending
|
|
1332
1542
|
* Useful for debugging and ensuring events are properly formatted
|