@seekora-ai/search-sdk 0.2.6 → 0.2.8

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 CHANGED
@@ -54,6 +54,58 @@ export interface SeekoraClientConfig {
54
54
  * Event queue configuration
55
55
  */
56
56
  eventQueue?: EventQueueConfig;
57
+ /**
58
+ * Default options for getSuggestions(); merged with per-call options (per-call overrides).
59
+ * Use to set e.g. include_dropdown_product_list: false, include_filtered_tabs: false for all suggestion calls.
60
+ */
61
+ suggestionsDefaults?: {
62
+ include_dropdown_recommendations?: boolean;
63
+ include_dropdown_product_list?: boolean;
64
+ include_filtered_tabs?: boolean;
65
+ hitsPerPage?: number;
66
+ time_range?: '7d' | '30d' | '90d';
67
+ [key: string]: unknown;
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;
57
109
  }
58
110
  /**
59
111
  * Search context containing identifiers for linking events to searches
@@ -66,6 +118,8 @@ export interface SearchContext {
66
118
  correlationId: string;
67
119
  /** Search ID from backend (if present in response) */
68
120
  searchId?: string;
121
+ /** Journey ID for session-level tracking */
122
+ journey_id?: string;
69
123
  /** User ID if authenticated */
70
124
  userId?: string;
71
125
  /** Anonymous user ID */
@@ -89,6 +143,10 @@ export interface ExtendedEventPayload extends DataTypesEventPayload {
89
143
  correlation_id?: string;
90
144
  /** Search ID at top level (also in metadata for backward compat) */
91
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;
92
150
  }
93
151
  export interface SearchOptions {
94
152
  q: string;
@@ -269,6 +327,9 @@ export declare class SeekoraClient {
269
327
  private cachedBrowserContext;
270
328
  private enableEventQueue;
271
329
  private eventQueue;
330
+ private clientConfig;
331
+ private enableDeduplication;
332
+ private deduplicationWindow;
272
333
  constructor(config?: SeekoraClientConfig);
273
334
  /**
274
335
  * Search for documents
@@ -304,6 +365,10 @@ export declare class SeekoraClient {
304
365
  time_range?: '7d' | '30d' | '90d';
305
366
  disable_typo_tolerance?: boolean;
306
367
  include_dropdown_recommendations?: boolean;
368
+ /** When false, omit product hits list from dropdown result (default true) */
369
+ include_dropdown_product_list?: boolean;
370
+ /** When false, omit filtered_tabs from dropdown extensions (default true) */
371
+ include_filtered_tabs?: boolean;
307
372
  filtered_tabs?: Array<{
308
373
  id?: string;
309
374
  label: string;
@@ -456,6 +521,11 @@ export declare class SeekoraClient {
456
521
  * Send a batch of events directly to the backend (bypasses queue)
457
522
  */
458
523
  private sendEventsBatchDirect;
524
+ /**
525
+ * Automatically track search result impressions
526
+ * Creates a batch of view events for all items in search results
527
+ */
528
+ private autoTrackSearchImpressions;
459
529
  /**
460
530
  * Get browser context (sync if cached, otherwise returns null)
461
531
  * Use collectBrowserContext() for async collection
@@ -465,6 +535,19 @@ export declare class SeekoraClient {
465
535
  * Collect browser context asynchronously
466
536
  */
467
537
  collectBrowserContext(): Promise<BrowserContext>;
538
+ /**
539
+ * Generate insert_id for event deduplication (industry standard)
540
+ *
541
+ * Format: {user_key}_{event_type}_{item_id}_{timestamp_window}
542
+ * Similar to Segment, Amplitude, Mixpanel deduplication keys
543
+ *
544
+ * @param eventType - Event name (search, click, conversion, etc.)
545
+ * @param itemId - Item ID (optional, for item-specific events)
546
+ * @param userId - User ID or anonymous ID
547
+ * @param window - Deduplication window in milliseconds
548
+ * @returns insert_id string for backend deduplication
549
+ */
550
+ private generateInsertId;
468
551
  /**
469
552
  * Build event payload with identifiers and browser context
470
553
  * Ensures user_id or anon_id is present, and sets correlation_id/search_id at top level
@@ -569,6 +652,53 @@ export declare class SeekoraClient {
569
652
  * @param context - Optional search context for linking events to searches
570
653
  */
571
654
  trackCustom(eventName: string, payload?: Partial<DataTypesEventPayload>, context?: SearchContext): Promise<void>;
655
+ /**
656
+ * Track add to cart conversion event
657
+ *
658
+ * @param params - Add to cart parameters
659
+ * @param params.itemId - Item ID that was added to cart
660
+ * @param params.quantity - Quantity added (default: 1)
661
+ * @param params.value - Item value/price
662
+ * @param params.currency - Currency code (default: 'USD')
663
+ * @param params.position - Position in search results (if from search)
664
+ * @param params.searchContext - Search context for attribution
665
+ */
666
+ trackAddToCart(params: {
667
+ itemId: string;
668
+ quantity?: number;
669
+ value?: number;
670
+ currency?: string;
671
+ position?: number;
672
+ searchContext?: SearchContext;
673
+ }): Promise<void>;
674
+ /**
675
+ * Track add to wishlist conversion event
676
+ *
677
+ * @param params - Add to wishlist parameters
678
+ * @param params.itemId - Item ID that was added to wishlist
679
+ * @param params.position - Position in search results (if from search)
680
+ * @param params.searchContext - Search context for attribution
681
+ */
682
+ trackAddToWishlist(params: {
683
+ itemId: string;
684
+ position?: number;
685
+ searchContext?: SearchContext;
686
+ }): Promise<void>;
687
+ /**
688
+ * Track item share event
689
+ *
690
+ * @param params - Share parameters
691
+ * @param params.itemId - Item ID that was shared
692
+ * @param params.shareMethod - Share method (e.g., 'facebook', 'twitter', 'whatsapp', 'copy_link')
693
+ * @param params.position - Position in search results (if from search)
694
+ * @param params.searchContext - Search context for attribution
695
+ */
696
+ trackShare(params: {
697
+ itemId: string;
698
+ shareMethod?: string;
699
+ position?: number;
700
+ searchContext?: SearchContext;
701
+ }): Promise<void>;
572
702
  /**
573
703
  * Validate an event payload before sending
574
704
  * Useful for debugging and ensuring events are properly formatted
package/dist/client.js CHANGED
@@ -25,6 +25,7 @@ class SeekoraClient {
25
25
  constructor(config = {}) {
26
26
  this.cachedBrowserContext = null;
27
27
  this.eventQueue = null;
28
+ this.clientConfig = config;
28
29
  // Load configuration from file, env, and code (in that order)
29
30
  const mergedConfig = (0, config_loader_1.loadConfig)(config);
30
31
  this.storeId = mergedConfig.storeId;
@@ -55,6 +56,9 @@ class SeekoraClient {
55
56
  this.eventQueue.setLogger(this.logger);
56
57
  this.eventQueue.setSender(this.createQueueSender());
57
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
58
62
  // Log identifier initialization
59
63
  this.logger.verbose('Client identifiers initialized', {
60
64
  hasUserId: !!this.userId,
@@ -62,7 +66,9 @@ class SeekoraClient {
62
66
  sessionId: this.sessionId.substring(0, 8) + '...',
63
67
  autoTrackSearch: this.autoTrackSearch,
64
68
  enableContextCollection: this.enableContextCollection,
65
- enableEventQueue: this.enableEventQueue
69
+ enableEventQueue: this.enableEventQueue,
70
+ enableDeduplication: this.enableDeduplication,
71
+ deduplicationWindow: this.deduplicationWindow
66
72
  });
67
73
  this.logger.verbose('Initializing SeekoraClient', {
68
74
  storeId: this.storeId,
@@ -274,6 +280,22 @@ class SeekoraClient {
274
280
  this.logger.warn('Failed to auto-track search event', { error: err.message });
275
281
  });
276
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
+ }
277
299
  return results;
278
300
  }
279
301
  catch (error) {
@@ -324,20 +346,25 @@ class SeekoraClient {
324
346
  if (this.sessionId)
325
347
  headers['x-session-id'] = this.sessionId;
326
348
  const usePost = (options?.filtered_tabs && options.filtered_tabs.length > 0) ||
327
- options?.include_dropdown_recommendations === true;
349
+ options?.include_dropdown_recommendations === true ||
350
+ options?.include_dropdown_product_list === false ||
351
+ options?.include_filtered_tabs === false;
352
+ const defaults = this.clientConfig?.suggestionsDefaults;
328
353
  const buildRequestBody = () => {
329
354
  const body = {
330
355
  query,
331
- hitsPerPage: options?.hitsPerPage || 5,
356
+ hitsPerPage: options?.hitsPerPage ?? defaults?.hitsPerPage ?? 5,
332
357
  page: options?.page,
333
358
  include_categories: options?.include_categories,
334
359
  include_facets: options?.include_facets,
335
360
  max_categories: options?.max_categories,
336
361
  max_facets: options?.max_facets,
337
362
  min_popularity: options?.min_popularity,
338
- time_range: options?.time_range,
363
+ time_range: options?.time_range ?? defaults?.time_range,
339
364
  disable_typo_tolerance: options?.disable_typo_tolerance,
340
- include_dropdown_recommendations: options?.include_dropdown_recommendations,
365
+ include_dropdown_recommendations: options?.include_dropdown_recommendations ?? defaults?.include_dropdown_recommendations,
366
+ include_dropdown_product_list: options?.include_dropdown_product_list ?? defaults?.include_dropdown_product_list,
367
+ include_filtered_tabs: options?.include_filtered_tabs ?? defaults?.include_filtered_tabs,
341
368
  filtered_tabs: options?.filtered_tabs,
342
369
  };
343
370
  if (options?.analytics_tags) {
@@ -1020,6 +1047,52 @@ class SeekoraClient {
1020
1047
  throw new Error(`Failed to send batch events: ${response.status}`);
1021
1048
  }
1022
1049
  }
1050
+ /**
1051
+ * Automatically track search result impressions
1052
+ * Creates a batch of view events for all items in search results
1053
+ */
1054
+ async autoTrackSearchImpressions(searchResponse, context) {
1055
+ const items = searchResponse.results || [];
1056
+ if (items.length === 0) {
1057
+ return;
1058
+ }
1059
+ this.logger.verbose('Auto-tracking search impressions', {
1060
+ itemCount: items.length,
1061
+ query: searchResponse.query || searchResponse.data?.data?.query,
1062
+ searchId: context.searchId
1063
+ });
1064
+ // Create batch impression events
1065
+ const impressionEvents = items.map((item, index) => ({
1066
+ event_name: 'view',
1067
+ event_id: (0, utils_1.generateUUID)(),
1068
+ event_ts: new Date().toISOString(),
1069
+ // Link to search
1070
+ search_id: context.searchId,
1071
+ journey_id: context.journey_id,
1072
+ correlation_id: context.correlationId,
1073
+ session_id: context.sessionId || this.sessionId,
1074
+ // User context
1075
+ user_id: context.userId || this.userId,
1076
+ anonymous_id: context.anonId || this.anonId,
1077
+ // Item details
1078
+ clicked_item_id: item.id,
1079
+ position: index + 1,
1080
+ query: searchResponse.query || searchResponse.data?.data?.query,
1081
+ // Optional metadata
1082
+ analytics_tags: ['source:search', 'auto_tracked:true']
1083
+ }));
1084
+ try {
1085
+ await this.trackEvents(impressionEvents);
1086
+ this.logger.debug('Auto-tracked search impressions', { count: impressionEvents.length });
1087
+ }
1088
+ catch (error) {
1089
+ this.logger.error('Failed to auto-track impressions', {
1090
+ error: error.message,
1091
+ itemCount: items.length
1092
+ });
1093
+ throw error;
1094
+ }
1095
+ }
1023
1096
  /**
1024
1097
  * Get browser context (sync if cached, otherwise returns null)
1025
1098
  * Use collectBrowserContext() for async collection
@@ -1034,6 +1107,31 @@ class SeekoraClient {
1034
1107
  this.cachedBrowserContext = await this.contextCollector.collect();
1035
1108
  return this.cachedBrowserContext;
1036
1109
  }
1110
+ /**
1111
+ * Generate insert_id for event deduplication (industry standard)
1112
+ *
1113
+ * Format: {user_key}_{event_type}_{item_id}_{timestamp_window}
1114
+ * Similar to Segment, Amplitude, Mixpanel deduplication keys
1115
+ *
1116
+ * @param eventType - Event name (search, click, conversion, etc.)
1117
+ * @param itemId - Item ID (optional, for item-specific events)
1118
+ * @param userId - User ID or anonymous ID
1119
+ * @param window - Deduplication window in milliseconds
1120
+ * @returns insert_id string for backend deduplication
1121
+ */
1122
+ generateInsertId(eventType, itemId, userId, window) {
1123
+ // Round timestamp to nearest window (5 min default)
1124
+ const timestamp = Math.floor(Date.now() / window);
1125
+ // Format: userKey_eventType_itemId_timestampWindow
1126
+ // Item ID is optional (not all events have items)
1127
+ const parts = [
1128
+ userId.replace(/[^a-zA-Z0-9]/g, '_'), // Sanitize user ID
1129
+ eventType.replace(/[^a-zA-Z0-9]/g, '_'), // Sanitize event type
1130
+ itemId ? itemId.replace(/[^a-zA-Z0-9]/g, '_') : 'null',
1131
+ timestamp.toString()
1132
+ ];
1133
+ return parts.join('_');
1134
+ }
1037
1135
  /**
1038
1136
  * Build event payload with identifiers and browser context
1039
1137
  * Ensures user_id or anon_id is present, and sets correlation_id/search_id at top level
@@ -1115,6 +1213,26 @@ class SeekoraClient {
1115
1213
  payload.is_tablet = ctx.is_tablet ? 1 : 0;
1116
1214
  payload.is_touch_device = ctx.is_touch_device;
1117
1215
  }
1216
+ // Generate insert_id for deduplication (industry standard: Segment/Amplitude pattern)
1217
+ // Always enable for purchase events (revenue must be accurate)
1218
+ // Optional for other events based on config
1219
+ const isPurchaseEvent = payload.event_name === 'conversion' &&
1220
+ (payload.conversion_type === 'purchase' || payload.conversion_type === 'checkout_completed');
1221
+ if (this.enableDeduplication || isPurchaseEvent) {
1222
+ const userKey = payload.user_id || payload.anon_id || this.anonId;
1223
+ const eventType = payload.event_name || 'unknown';
1224
+ const itemId = payload.clicked_item_id;
1225
+ // Use longer window for purchase events (30 days)
1226
+ const window = isPurchaseEvent ? 2592000000 : this.deduplicationWindow;
1227
+ payload.insert_id = this.generateInsertId(eventType, itemId, userKey, window);
1228
+ this.logger.verbose('Generated insert_id for deduplication', {
1229
+ event_name: eventType,
1230
+ item_id: itemId,
1231
+ insert_id: payload.insert_id,
1232
+ is_purchase: isPurchaseEvent,
1233
+ window_ms: window
1234
+ });
1235
+ }
1118
1236
  return payload;
1119
1237
  }
1120
1238
  /**
@@ -1321,6 +1439,82 @@ class SeekoraClient {
1321
1439
  event_name: eventName,
1322
1440
  }, context);
1323
1441
  }
1442
+ /**
1443
+ * Track add to cart conversion event
1444
+ *
1445
+ * @param params - Add to cart parameters
1446
+ * @param params.itemId - Item ID that was added to cart
1447
+ * @param params.quantity - Quantity added (default: 1)
1448
+ * @param params.value - Item value/price
1449
+ * @param params.currency - Currency code (default: 'USD')
1450
+ * @param params.position - Position in search results (if from search)
1451
+ * @param params.searchContext - Search context for attribution
1452
+ */
1453
+ async trackAddToCart(params) {
1454
+ this.logger.verbose('Tracking add to cart event', {
1455
+ itemId: params.itemId,
1456
+ quantity: params.quantity,
1457
+ value: params.value
1458
+ });
1459
+ await this.trackEvent({
1460
+ event_name: 'conversion',
1461
+ conversion_type: 'add_to_cart',
1462
+ clicked_item_id: params.itemId,
1463
+ quantity: params.quantity || 1,
1464
+ value: params.value,
1465
+ currency: params.currency || 'USD',
1466
+ position: params.position,
1467
+ analytics_tags: ['action:add_to_cart'],
1468
+ }, params.searchContext);
1469
+ }
1470
+ /**
1471
+ * Track add to wishlist conversion event
1472
+ *
1473
+ * @param params - Add to wishlist parameters
1474
+ * @param params.itemId - Item ID that was added to wishlist
1475
+ * @param params.position - Position in search results (if from search)
1476
+ * @param params.searchContext - Search context for attribution
1477
+ */
1478
+ async trackAddToWishlist(params) {
1479
+ this.logger.verbose('Tracking add to wishlist event', {
1480
+ itemId: params.itemId
1481
+ });
1482
+ await this.trackEvent({
1483
+ event_name: 'conversion',
1484
+ conversion_type: 'wishlist',
1485
+ clicked_item_id: params.itemId,
1486
+ position: params.position,
1487
+ analytics_tags: ['action:add_to_wishlist'],
1488
+ }, params.searchContext);
1489
+ }
1490
+ /**
1491
+ * Track item share event
1492
+ *
1493
+ * @param params - Share parameters
1494
+ * @param params.itemId - Item ID that was shared
1495
+ * @param params.shareMethod - Share method (e.g., 'facebook', 'twitter', 'whatsapp', 'copy_link')
1496
+ * @param params.position - Position in search results (if from search)
1497
+ * @param params.searchContext - Search context for attribution
1498
+ */
1499
+ async trackShare(params) {
1500
+ this.logger.verbose('Tracking share event', {
1501
+ itemId: params.itemId,
1502
+ shareMethod: params.shareMethod
1503
+ });
1504
+ await this.trackEvent({
1505
+ event_name: 'custom',
1506
+ clicked_item_id: params.itemId,
1507
+ position: params.position,
1508
+ analytics_tags: [
1509
+ 'action:share',
1510
+ `share_method:${params.shareMethod || 'unknown'}`
1511
+ ],
1512
+ custom_json: JSON.stringify({
1513
+ share_method: params.shareMethod,
1514
+ action_type: 'share'
1515
+ }),
1516
+ }, params.searchContext);
1517
+ }
1324
1518
  /**
1325
1519
  * Validate an event payload before sending
1326
1520
  * Useful for debugging and ensuring events are properly formatted