@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.
@@ -1,1548 +0,0 @@
1
- "use strict";
2
- /**
3
- * Seekora SDK Client
4
- * High-level wrapper around generated OpenAPI client
5
- */
6
- var __importDefault = (this && this.__importDefault) || function (mod) {
7
- return (mod && mod.__esModule) ? mod : { "default": mod };
8
- };
9
- Object.defineProperty(exports, "__esModule", { value: true });
10
- exports.SeekoraClient = void 0;
11
- const generated_1 = require("../generated");
12
- const axios_1 = __importDefault(require("axios"));
13
- const config_1 = require("./config");
14
- const logger_1 = require("./logger");
15
- const config_loader_1 = require("./config-loader");
16
- const utils_1 = require("./utils");
17
- const context_collector_1 = require("./context-collector");
18
- const event_queue_1 = require("./event-queue");
19
- /**
20
- * Seekora SDK Client
21
- *
22
- * Provides a clean, easy-to-use interface for the Seekora Search API
23
- */
24
- class SeekoraClient {
25
- constructor(config = {}) {
26
- this.cachedBrowserContext = null;
27
- this.eventQueue = null;
28
- // Load configuration from file, env, and code (in that order)
29
- const mergedConfig = (0, config_loader_1.loadConfig)(config);
30
- this.storeId = mergedConfig.storeId;
31
- this.readSecret = mergedConfig.readSecret;
32
- this.writeSecret = mergedConfig.writeSecret;
33
- // Initialize logger FIRST (before using it)
34
- const logLevel = mergedConfig.logLevel || config.logLevel || (0, logger_1.getLogLevelFromEnv)();
35
- this.logger = (0, logger_1.createLogger)({
36
- level: logLevel,
37
- ...config.logger,
38
- });
39
- // Initialize identifiers
40
- this.userId = config.userId;
41
- this.anonId = config.anonId || (0, utils_1.getOrCreateAnonId)();
42
- this.sessionId = config.sessionId || (0, utils_1.getOrCreateSessionId)();
43
- this.autoTrackSearch = config.autoTrackSearch || false;
44
- // Initialize context collection
45
- this.enableContextCollection = config.enableContextCollection !== false; // Default to true
46
- this.contextCollector = new context_collector_1.ContextCollector(config.contextCollector);
47
- // Pre-collect browser context if enabled
48
- if (this.enableContextCollection) {
49
- this.collectContextAsync();
50
- }
51
- // Initialize event queue if enabled
52
- this.enableEventQueue = config.enableEventQueue || false;
53
- if (this.enableEventQueue) {
54
- this.eventQueue = new event_queue_1.EventQueue(config.eventQueue);
55
- this.eventQueue.setLogger(this.logger);
56
- this.eventQueue.setSender(this.createQueueSender());
57
- }
58
- // Log identifier initialization
59
- this.logger.verbose('Client identifiers initialized', {
60
- hasUserId: !!this.userId,
61
- anonId: this.anonId.substring(0, 8) + '...', // Log partial ID for privacy
62
- sessionId: this.sessionId.substring(0, 8) + '...',
63
- autoTrackSearch: this.autoTrackSearch,
64
- enableContextCollection: this.enableContextCollection,
65
- enableEventQueue: this.enableEventQueue
66
- });
67
- this.logger.verbose('Initializing SeekoraClient', {
68
- storeId: this.storeId,
69
- environment: mergedConfig.environment,
70
- logLevel,
71
- });
72
- // Get base URL from environment or config
73
- const baseUrl = (0, config_1.getBaseUrl)(mergedConfig.environment, mergedConfig.baseUrl);
74
- this.logger.verbose('Using base URL', { baseUrl });
75
- // Create configuration with base URL
76
- this.config = new generated_1.Configuration({
77
- basePath: baseUrl,
78
- baseOptions: {
79
- timeout: mergedConfig.timeout || 30000,
80
- headers: {
81
- 'x-storeid': this.storeId,
82
- 'x-storesecret': this.readSecret,
83
- }
84
- }
85
- });
86
- // Initialize API clients
87
- this.searchApi = new generated_1.SearchApi(this.config);
88
- this.suggestionsApi = new generated_1.QuerySuggestionsApi(this.config);
89
- this.suggestionsConfigApi = new generated_1.SDKQuerySuggestionsConfigApi(this.config);
90
- this.analyticsApi = new generated_1.AnalyticsEventsApi(this.config);
91
- this.storesApi = new generated_1.SDKStoreConfigApi(this.config);
92
- this.documentsApi = new generated_1.SDKDocumentsApi(this.config);
93
- this.schemaApi = new generated_1.SDKSchemaApi(this.config);
94
- this.logger.info('SeekoraClient initialized successfully');
95
- }
96
- /**
97
- * Search for documents
98
- *
99
- * Generates a new correlation_id for this search and returns SearchContext
100
- * for linking subsequent events (clicks, conversions) to this search.
101
- *
102
- * @param query - Search query string
103
- * @param options - Search options (pagination, filters, facets, etc.)
104
- * @returns SearchResponse with results and SearchContext
105
- */
106
- async search(query, options) {
107
- // Convert empty query to wildcard
108
- const searchQuery = query.trim() || '*';
109
- // Generate correlation_id for this search journey
110
- const correlationId = (0, utils_1.generateUUID)();
111
- this.logger.verbose('Executing search', {
112
- originalQuery: query,
113
- searchQuery,
114
- correlationId,
115
- options
116
- });
117
- const searchRequest = {
118
- q: searchQuery,
119
- per_page: options?.per_page || 10,
120
- page: options?.page || 1,
121
- filter: options?.filter || options?.filter_by,
122
- facet_by: options?.facet_by,
123
- sort: options?.sort || options?.sort_by,
124
- analytics_tags: options?.analytics_tags,
125
- widget_mode: options?.widget_mode || false,
126
- // New advanced parameters
127
- search_fields: options?.search_fields,
128
- field_weights: options?.field_weights,
129
- return_fields: options?.return_fields,
130
- omit_fields: options?.omit_fields,
131
- snippet_fields: options?.snippet_fields,
132
- full_snippet_fields: options?.full_snippet_fields,
133
- snippet_prefix: options?.snippet_prefix,
134
- snippet_suffix: options?.snippet_suffix,
135
- snippet_token_limit: options?.snippet_token_limit,
136
- snippet_min_len: options?.snippet_min_len,
137
- include_snippets: options?.include_snippets,
138
- group_field: options?.group_field,
139
- group_size: options?.group_size,
140
- prefix_mode: options?.prefix_mode,
141
- infix_mode: options?.infix_mode,
142
- typo_max: options?.typo_max,
143
- typo_min_len_1: options?.typo_min_len_1,
144
- typo_min_len_2: options?.typo_min_len_2,
145
- search_timeout_ms: options?.search_timeout_ms,
146
- require_all_terms: options?.require_all_terms,
147
- exact_match_boost: options?.exact_match_boost,
148
- cache_results: options?.cache_results,
149
- apply_rules: options?.apply_rules,
150
- preset_name: options?.preset_name,
151
- facet_search_text: options?.facet_search_text,
152
- include_suggestions: options?.include_suggestions,
153
- suggestions_limit: options?.suggestions_limit,
154
- max_facet_values: options?.max_facet_values,
155
- stopword_sets: options?.stopword_sets,
156
- synonym_sets: options?.synonym_sets,
157
- };
158
- // Log search request details (verbose level for debugging)
159
- this.logger.verbose('Search request prepared', {
160
- filter: searchRequest.filter,
161
- filterType: typeof searchRequest.filter,
162
- fullRequest: searchRequest
163
- });
164
- try {
165
- // Log API request start
166
- this.logger.verbose('Sending search API request', {
167
- endpoint: '/api/v1/search',
168
- method: 'POST',
169
- storeId: this.storeId
170
- });
171
- // Build headers with personalization support
172
- const headers = {
173
- 'x-storeid': this.storeId,
174
- 'x-storesecret': this.readSecret,
175
- };
176
- // Add personalization headers if available
177
- if (this.userId) {
178
- headers['x-user-id'] = this.userId;
179
- }
180
- if (this.anonId) {
181
- headers['x-anon-id'] = this.anonId;
182
- }
183
- if (this.sessionId) {
184
- headers['x-session-id'] = this.sessionId;
185
- }
186
- const response = await this.searchApi.v1SearchPost(this.storeId, this.readSecret, searchRequest, this.userId, this.anonId, this.sessionId, {
187
- headers
188
- });
189
- // Log API response received
190
- this.logger.verbose('Search API response received', {
191
- status: response.status,
192
- hasData: !!response.data
193
- });
194
- // Extract search results
195
- const data = response.data;
196
- const facets = data?.data?.facets || [];
197
- // Log API response (verbose level for detailed debugging)
198
- this.logger.verbose('API response received', {
199
- status: response.status,
200
- totalResults: data?.data?.total_results || 0,
201
- resultsCount: data?.data?.results?.length || 0,
202
- facetsType: Array.isArray(facets) ? 'array' : typeof facets,
203
- facetsCount: Array.isArray(facets) ? facets.length : Object.keys(facets || {}).length
204
- });
205
- // Convert facets array to a more accessible object format
206
- const facetsMap = {};
207
- if (Array.isArray(facets)) {
208
- this.logger.verbose('Processing facets as array', { facetsCount: facets.length });
209
- facets.forEach((facet, index) => {
210
- // Handle different facet formats
211
- const fieldName = facet?.field_name || facet?.fieldName || facet?.name || `facet_${index}`;
212
- if (fieldName && (facet?.counts || facet?.values || Array.isArray(facet))) {
213
- facetsMap[fieldName] = {
214
- field_name: fieldName,
215
- counts: facet.counts || facet.values || (Array.isArray(facet) ? facet : []),
216
- stats: facet.stats || {}
217
- };
218
- this.logger.verbose('Processed facet', { fieldName, index, counts: facetsMap[fieldName].counts?.length || 0 });
219
- }
220
- });
221
- }
222
- else if (facets && typeof facets === 'object' && !Array.isArray(facets)) {
223
- this.logger.verbose('Processing facets as object', { facetKeys: Object.keys(facets) });
224
- // If facets is already an object, use it directly but ensure field_name is set
225
- Object.keys(facets).forEach((key) => {
226
- const facetData = facets[key];
227
- if (facetData) {
228
- facetsMap[key] = {
229
- field_name: facetData.field_name || key,
230
- counts: facetData.counts || facetData.values || [],
231
- stats: facetData.stats || {}
232
- };
233
- }
234
- });
235
- }
236
- this.logger.verbose('Facets processed', {
237
- facetFields: Object.keys(facetsMap),
238
- totalFacetFields: Object.keys(facetsMap).length
239
- });
240
- // Extract search_id from response (if present)
241
- const searchId = data?.data?.search_id;
242
- // Create search context for linking events
243
- const context = {
244
- correlationId,
245
- searchId,
246
- userId: this.userId,
247
- anonId: this.anonId,
248
- sessionId: this.sessionId,
249
- };
250
- const results = {
251
- results: data?.data?.results || [],
252
- totalResults: data?.data?.total_results || 0,
253
- searchId, // Keep for backward compatibility
254
- context, // New: search context for event linking
255
- facets: facetsMap,
256
- facet_counts: facetsMap, // Alias for backward compatibility
257
- ...data
258
- };
259
- this.logger.info('Search completed', {
260
- query: searchQuery,
261
- originalQuery: query,
262
- totalResults: results.totalResults,
263
- searchId: results.searchId,
264
- correlationId,
265
- });
266
- // Automatically track search event if enabled
267
- if (this.autoTrackSearch) {
268
- this.trackSearch({
269
- query: searchQuery,
270
- resultsCount: results.totalResults,
271
- context,
272
- }).catch((err) => {
273
- // Log but don't fail the search if tracking fails
274
- this.logger.warn('Failed to auto-track search event', { error: err.message });
275
- });
276
- }
277
- return results;
278
- }
279
- catch (error) {
280
- // Log detailed error information
281
- const errorDetails = {
282
- query: searchQuery,
283
- originalQuery: query,
284
- error: error.message,
285
- };
286
- if (error.response) {
287
- errorDetails.status = error.response.status;
288
- errorDetails.responseData = error.response.data;
289
- errorDetails.requestUrl = error.config?.url;
290
- errorDetails.requestMethod = error.config?.method;
291
- }
292
- else if (error.request) {
293
- errorDetails.networkError = true;
294
- errorDetails.requestConfig = {
295
- url: error.config?.url,
296
- method: error.config?.method,
297
- };
298
- }
299
- this.logger.error('Search failed', errorDetails);
300
- throw this.handleError(error, 'search');
301
- }
302
- }
303
- /**
304
- * Get query suggestions
305
- *
306
- * Uses POST when filtered_tabs or include_dropdown_recommendations is set (GET does not send include_dropdown_recommendations).
307
- * With returnFullResponse: true returns full shape including extensions (trending_searches, top_searches, related_searches, popular_brands, filtered_tabs).
308
- *
309
- * @param query - Partial query to get suggestions for
310
- * @param options - Optional parameters for suggestions
311
- * @returns Suggestion hits array, or QuerySuggestionsFullResponse when returnFullResponse is true
312
- */
313
- async getSuggestions(query, options) {
314
- this.logger.verbose('Getting query suggestions', { query, options });
315
- try {
316
- const headers = {
317
- 'x-storeid': this.storeId,
318
- 'x-storesecret': this.readSecret,
319
- };
320
- if (this.userId)
321
- headers['x-user-id'] = this.userId;
322
- if (this.anonId)
323
- headers['x-anon-id'] = this.anonId;
324
- if (this.sessionId)
325
- headers['x-session-id'] = this.sessionId;
326
- const usePost = (options?.filtered_tabs && options.filtered_tabs.length > 0) ||
327
- options?.include_dropdown_recommendations === true;
328
- const buildRequestBody = () => {
329
- const body = {
330
- query,
331
- hitsPerPage: options?.hitsPerPage || 5,
332
- page: options?.page,
333
- include_categories: options?.include_categories,
334
- include_facets: options?.include_facets,
335
- max_categories: options?.max_categories,
336
- max_facets: options?.max_facets,
337
- min_popularity: options?.min_popularity,
338
- time_range: options?.time_range,
339
- disable_typo_tolerance: options?.disable_typo_tolerance,
340
- include_dropdown_recommendations: options?.include_dropdown_recommendations,
341
- filtered_tabs: options?.filtered_tabs,
342
- };
343
- if (options?.analytics_tags) {
344
- body.analytics_tags = Array.isArray(options.analytics_tags)
345
- ? options.analytics_tags
346
- : [options.analytics_tags];
347
- }
348
- if (options?.tags_match_mode)
349
- body.tags_match_mode = options.tags_match_mode;
350
- return body;
351
- };
352
- if (usePost) {
353
- this.logger.verbose('Using POST endpoint (filtered_tabs or include_dropdown_recommendations)', {
354
- endpoint: '/api/v1/suggestions/queries',
355
- query,
356
- });
357
- const requestBody = buildRequestBody();
358
- const response = await this.suggestionsApi.v1SuggestionsQueriesPost(this.storeId, this.readSecret, this.userId, this.anonId, this.sessionId, requestBody, { headers });
359
- const responseData = response.data?.data || response.data;
360
- const suggestions = responseData?.results?.[0]?.hits || responseData?.hits || [];
361
- const extensions = responseData?.results?.[1]?.extensions ?? responseData?.results?.[0]?.extensions;
362
- if (options?.returnFullResponse) {
363
- this.logger.info('Query suggestions retrieved (POST, full response)', {
364
- query,
365
- count: suggestions.length,
366
- status: response.status,
367
- });
368
- return {
369
- suggestions,
370
- results: responseData?.results,
371
- extensions: extensions ?? undefined,
372
- raw: responseData,
373
- };
374
- }
375
- this.logger.info('Query suggestions retrieved (POST)', {
376
- query,
377
- count: suggestions.length,
378
- status: response.status,
379
- });
380
- return suggestions;
381
- }
382
- // GET for simple requests (no filtered_tabs, no include_dropdown_recommendations)
383
- this.logger.verbose('Using GET endpoint', {
384
- endpoint: '/api/v1/suggestions/queries',
385
- query,
386
- });
387
- const analyticsTags = Array.isArray(options?.analytics_tags)
388
- ? options.analytics_tags.join(',')
389
- : options?.analytics_tags;
390
- const response = await this.suggestionsApi.v1SuggestionsQueriesGet(this.storeId, this.readSecret, this.userId, this.anonId, this.sessionId, query, options?.hitsPerPage || 5, options?.page, analyticsTags, options?.tags_match_mode, options?.include_categories, options?.include_facets, options?.max_categories, options?.max_facets, options?.min_popularity, options?.time_range, options?.disable_typo_tolerance, { headers });
391
- const responseData = response.data?.data || response.data;
392
- const suggestions = responseData?.results?.[0]?.hits || responseData?.hits || [];
393
- if (options?.returnFullResponse) {
394
- const extensions = responseData?.results?.[1]?.extensions ?? responseData?.results?.[0]?.extensions;
395
- this.logger.info('Query suggestions retrieved (GET, full response)', {
396
- query,
397
- count: suggestions.length,
398
- status: response.status,
399
- });
400
- return {
401
- suggestions,
402
- results: responseData?.results,
403
- extensions: extensions ?? undefined,
404
- raw: responseData,
405
- };
406
- }
407
- this.logger.info('Query suggestions retrieved (GET)', {
408
- query,
409
- count: suggestions.length,
410
- status: response.status,
411
- });
412
- return suggestions;
413
- }
414
- catch (error) {
415
- this.logger.error('Failed to get suggestions', { query, error: error.message });
416
- throw this.handleError(error, 'getSuggestions');
417
- }
418
- }
419
- /**
420
- * Get query suggestions configuration (store-specific).
421
- * Uses GET /api/v1/stores/{storeId}/query-suggestions/config via SDKQuerySuggestionsConfigApi.
422
- */
423
- async getSuggestionsConfig() {
424
- this.logger.verbose('Getting suggestions configuration');
425
- try {
426
- const headers = {
427
- 'x-storeid': this.storeId,
428
- 'x-storesecret': this.readSecret,
429
- };
430
- try {
431
- const response = await this.suggestionsConfigApi.apiV1StoresXStoreIDQuerySuggestionsConfigGet(this.storeId, this.readSecret, this.storeId, { headers });
432
- const data = response.data?.data;
433
- const config = data?.metadata?.config ?? data?.config ?? data;
434
- this.logger.info('Suggestions configuration retrieved', {
435
- configKeys: config ? Object.keys(config) : [],
436
- status: response.status,
437
- });
438
- return config;
439
- }
440
- catch (e) {
441
- this.logger.verbose('Store config endpoint failed, trying legacy endpoint', { message: e?.message });
442
- const response = await this.suggestionsApi.v1SuggestionsConfigGet(this.storeId, this.readSecret, { headers });
443
- const config = response.data?.data;
444
- this.logger.info('Suggestions configuration retrieved (legacy)', {
445
- configKeys: config ? Object.keys(config) : [],
446
- status: response.status,
447
- });
448
- return config;
449
- }
450
- }
451
- catch (error) {
452
- this.logger.error('Failed to get suggestions config', { error: error.message });
453
- throw this.handleError(error, 'getSuggestionsConfig');
454
- }
455
- }
456
- /**
457
- * Get store configuration
458
- * Returns store search configuration and onboarding status
459
- */
460
- async getConfig() {
461
- this.logger.verbose('Getting store configuration');
462
- try {
463
- this.logger.verbose('Fetching store configuration', {
464
- endpoint: `/api/v1/stores/${this.storeId}/config`
465
- });
466
- // Build headers with personalization support
467
- const headers = {
468
- 'x-storeid': this.storeId,
469
- 'x-storesecret': this.readSecret,
470
- };
471
- // Add personalization headers if available
472
- if (this.userId) {
473
- headers['x-user-id'] = this.userId;
474
- }
475
- if (this.anonId) {
476
- headers['x-anon-id'] = this.anonId;
477
- }
478
- if (this.sessionId) {
479
- headers['x-session-id'] = this.sessionId;
480
- }
481
- const response = await this.storesApi.apiV1StoresXStoreIDConfigGet(this.storeId, this.readSecret, this.storeId, { headers });
482
- const config = response.data?.data;
483
- this.logger.info('Store configuration retrieved', {
484
- status: response.status,
485
- hasConfig: !!config
486
- });
487
- return config;
488
- }
489
- catch (error) {
490
- this.logger.error('Failed to get store config', { error: error.message });
491
- throw this.handleError(error, 'getConfig');
492
- }
493
- }
494
- /**
495
- * Get store information
496
- * Returns store metadata including name, status, and configuration details
497
- *
498
- * Note: This extracts information from the store config response.
499
- * Store name may be derived from config fields if not directly available.
500
- *
501
- * @returns Store information object
502
- */
503
- async getStoreInfo() {
504
- this.logger.verbose('Getting store information');
505
- try {
506
- const configResponse = await this.getConfig();
507
- // Extract store info from config response
508
- const storeInfo = {
509
- storeId: this.storeId,
510
- };
511
- // The config response structure: { config: IndexConfig, onboarding_status: string }
512
- if (configResponse) {
513
- // Handle different response structures
514
- const config = configResponse.config || configResponse;
515
- const onboardingStatus = configResponse.onboarding_status;
516
- if (onboardingStatus) {
517
- storeInfo.onboardingStatus = onboardingStatus;
518
- }
519
- if (config) {
520
- // Extract store name from various possible fields
521
- storeInfo.storeName =
522
- config.store_name ||
523
- config.storeName ||
524
- config.name ||
525
- config.title ||
526
- config.collection_name ||
527
- `Store ${this.storeId.substring(0, 8).toUpperCase()}`;
528
- // Extract query and facet fields
529
- if (config.query_by) {
530
- storeInfo.queryFields = Array.isArray(config.query_by)
531
- ? config.query_by
532
- : [config.query_by];
533
- }
534
- if (config.facet_by) {
535
- storeInfo.facetFields = Array.isArray(config.facet_by)
536
- ? config.facet_by
537
- : [config.facet_by];
538
- }
539
- if (config.primary_text) {
540
- storeInfo.primaryField = config.primary_text;
541
- }
542
- // Store the full config for reference
543
- storeInfo.config = config;
544
- }
545
- }
546
- this.logger.info('Store information retrieved', {
547
- storeId: storeInfo.storeId,
548
- storeName: storeInfo.storeName
549
- });
550
- return storeInfo;
551
- }
552
- catch (error) {
553
- this.logger.error('Failed to get store info', { error: error.message });
554
- throw this.handleError(error, 'getStoreInfo');
555
- }
556
- }
557
- /**
558
- * Update store configuration (requires write secret)
559
- */
560
- async updateConfig(config) {
561
- this.logger.verbose('Updating store configuration', { configKeys: Object.keys(config) });
562
- if (!this.writeSecret) {
563
- this.logger.error('Write secret required but not provided', {
564
- operation: 'updateConfig',
565
- storeId: this.storeId
566
- });
567
- throw new Error('Write secret is required for updateConfig');
568
- }
569
- try {
570
- this.logger.verbose('Updating store configuration', {
571
- endpoint: `/api/v1/stores/${this.storeId}/config`,
572
- configKeys: Object.keys(config)
573
- });
574
- // Build headers with write secret
575
- const headers = {
576
- 'x-storeid': this.storeId,
577
- 'x-storesecret': this.readSecret,
578
- 'x-store-write-secret': this.writeSecret,
579
- };
580
- const response = await this.storesApi.apiV1StoresXStoreIDConfigPut(this.storeId, this.writeSecret, this.storeId, config, { headers });
581
- const updatedConfig = response.data?.data;
582
- this.logger.info('Store configuration updated successfully', {
583
- status: response.status,
584
- hasConfig: !!updatedConfig
585
- });
586
- return updatedConfig;
587
- }
588
- catch (error) {
589
- this.logger.error('Failed to update store config', { error: error.message });
590
- throw this.handleError(error, 'updateConfig');
591
- }
592
- }
593
- /**
594
- * Update query suggestions configuration (requires write secret).
595
- * Uses PUT /api/v1/stores/{xStoreID}/query-suggestions/config via SDKQuerySuggestionsConfigApi.
596
- */
597
- async updateQuerySuggestionsConfig(config) {
598
- this.logger.verbose('Updating query suggestions configuration', { configKeys: Object.keys(config) });
599
- if (!this.writeSecret) {
600
- this.logger.error('Write secret required but not provided', {
601
- operation: 'updateQuerySuggestionsConfig',
602
- storeId: this.storeId,
603
- });
604
- throw new Error('Write secret is required for updateQuerySuggestionsConfig');
605
- }
606
- try {
607
- const headers = {
608
- 'x-storeid': this.storeId,
609
- 'x-store-write-secret': this.writeSecret,
610
- 'Content-Type': 'application/json',
611
- };
612
- try {
613
- const response = await this.suggestionsConfigApi.apiV1StoresXStoreIDQuerySuggestionsConfigPut(this.storeId, this.writeSecret, this.storeId, config, { headers });
614
- const data = response.data?.data;
615
- const updatedConfig = data?.metadata?.config ?? data?.config ?? data;
616
- this.logger.info('Query suggestions configuration updated successfully', {
617
- status: response.status,
618
- hasConfig: !!updatedConfig,
619
- });
620
- return updatedConfig;
621
- }
622
- catch (apiError) {
623
- this.logger.verbose('PUT via generated API failed, using direct HTTP', { message: apiError?.message });
624
- const axiosInstance = this.suggestionsConfigApi.axios ?? this.suggestionsApi.axios ?? axios_1.default;
625
- const baseUrl = this.config.basePath ?? 'https://api.seekora.com/api';
626
- const response = await axiosInstance.put(`${baseUrl}/v1/stores/${this.storeId}/query-suggestions/config`, config, { headers });
627
- const data = response.data?.data;
628
- const updatedConfig = data?.metadata?.config ?? data?.config ?? data;
629
- this.logger.info('Query suggestions configuration updated successfully', {
630
- status: response.status,
631
- hasConfig: !!updatedConfig,
632
- });
633
- return updatedConfig;
634
- }
635
- }
636
- catch (error) {
637
- this.logger.error('Failed to update query suggestions config', { error: error.message });
638
- throw this.handleError(error, 'updateQuerySuggestionsConfig');
639
- }
640
- }
641
- /**
642
- * Get configuration schema
643
- */
644
- async getConfigSchema() {
645
- this.logger.verbose('Getting configuration schema');
646
- try {
647
- const response = await this.storesApi.apiV1StoresXStoreIDConfigSchemaGet(this.storeId, this.readSecret, this.storeId, {
648
- headers: {
649
- 'x-storeid': this.storeId,
650
- 'x-storesecret': this.readSecret,
651
- }
652
- });
653
- const schema = response.data?.data;
654
- this.logger.verbose('Configuration schema retrieved', { schemaKeys: schema ? Object.keys(schema) : [] });
655
- return schema;
656
- }
657
- catch (error) {
658
- this.logger.error('Failed to get config schema', { error: error.message });
659
- throw this.handleError(error, 'getConfigSchema');
660
- }
661
- }
662
- // ==========================================
663
- // Document Indexing Methods
664
- // ==========================================
665
- /**
666
- * Index a single document into the store
667
- *
668
- * @param document - Document data with optional id
669
- * @returns IndexDocumentResponse with document id and status
670
- */
671
- async indexDocument(document) {
672
- this.logger.verbose('Indexing document', {
673
- hasCustomId: !!document.id,
674
- documentKeys: Object.keys(document.data)
675
- });
676
- if (!this.writeSecret) {
677
- this.logger.error('Write secret required but not provided', {
678
- operation: 'indexDocument',
679
- storeId: this.storeId
680
- });
681
- throw new Error('Write secret is required for indexDocument');
682
- }
683
- try {
684
- const request = {
685
- id: document.id,
686
- data: document.data,
687
- };
688
- const response = await this.documentsApi.apiV1StoresXStoreIDDocumentsPost(this.storeId, this.writeSecret, this.storeId, request);
689
- const responseData = response.data?.data;
690
- this.logger.info('Document indexed successfully', {
691
- id: responseData?.id || document.id,
692
- status: response.status
693
- });
694
- return {
695
- id: responseData?.id || document.id || '',
696
- success: responseData?.status === 'success' || responseData?.status === 'inserted' || responseData?.status === 'updated' || true,
697
- message: responseData?.message,
698
- data: responseData,
699
- };
700
- }
701
- catch (error) {
702
- this.logger.error('Failed to index document', { error: error.message });
703
- throw this.handleError(error, 'indexDocument');
704
- }
705
- }
706
- /**
707
- * Index multiple documents in bulk
708
- *
709
- * @param documents - Array of documents with optional actions (insert/update/upsert/delete)
710
- * @returns BulkIndexResponse with success/error counts
711
- */
712
- async indexDocuments(documents) {
713
- this.logger.verbose('Bulk indexing documents', {
714
- count: documents.length,
715
- actions: documents.map(d => d.action || 'upsert')
716
- });
717
- if (!this.writeSecret) {
718
- this.logger.error('Write secret required but not provided', {
719
- operation: 'indexDocuments',
720
- storeId: this.storeId
721
- });
722
- throw new Error('Write secret is required for indexDocuments');
723
- }
724
- try {
725
- const request = {
726
- documents: documents.map(doc => ({
727
- id: doc.id,
728
- action: doc.action,
729
- data: doc.data,
730
- })),
731
- };
732
- const response = await this.documentsApi.apiV1StoresXStoreIDDocumentsBulkPost(this.storeId, this.writeSecret, this.storeId, request);
733
- const responseData = response.data?.data;
734
- this.logger.info('Bulk document indexing completed', {
735
- successCount: responseData?.successCount || 0,
736
- errorCount: responseData?.errorCount || 0,
737
- status: response.status
738
- });
739
- return {
740
- success_count: responseData?.successCount || documents.length,
741
- error_count: responseData?.errorCount || 0,
742
- results: responseData?.results?.map(r => ({
743
- id: r.id,
744
- success: r.status === 'success' || r.status === 'inserted' || r.status === 'updated' || !r.error,
745
- error: r.error,
746
- })) || documents.map((doc) => ({
747
- id: doc.id,
748
- success: true,
749
- })),
750
- data: responseData,
751
- };
752
- }
753
- catch (error) {
754
- this.logger.error('Failed to bulk index documents', {
755
- count: documents.length,
756
- error: error.message
757
- });
758
- throw this.handleError(error, 'indexDocuments');
759
- }
760
- }
761
- /**
762
- * Delete a document by ID
763
- *
764
- * @param documentId - Document ID to delete
765
- */
766
- async deleteDocument(documentId) {
767
- this.logger.verbose('Deleting document', { documentId });
768
- if (!this.writeSecret) {
769
- this.logger.error('Write secret required but not provided', {
770
- operation: 'deleteDocument',
771
- storeId: this.storeId
772
- });
773
- throw new Error('Write secret is required for deleteDocument');
774
- }
775
- try {
776
- const response = await this.documentsApi.apiV1StoresXStoreIDDocumentsDocumentIDDelete(this.storeId, this.writeSecret, this.storeId, documentId);
777
- this.logger.info('Document deleted successfully', {
778
- documentId,
779
- status: response.status
780
- });
781
- }
782
- catch (error) {
783
- this.logger.error('Failed to delete document', {
784
- documentId,
785
- error: error.message
786
- });
787
- throw this.handleError(error, 'deleteDocument');
788
- }
789
- }
790
- // ==================
791
- // Schema Management
792
- // ==================
793
- /**
794
- * Create or update the collection schema for this store
795
- *
796
- * @param request - Schema request with fields and options
797
- * @returns SchemaResponse with the created/updated schema
798
- *
799
- * @example
800
- * // Create a new schema
801
- * const schema = await client.createSchema({
802
- * fields: [
803
- * { name: 'title', type: 'string' },
804
- * { name: 'content', type: 'string' },
805
- * { name: 'url', type: 'string' },
806
- * { name: 'hierarchy.lvl0', type: 'string', facet: true }
807
- * ]
808
- * });
809
- *
810
- * @example
811
- * // Add new fields to existing schema (additive mode)
812
- * const schema = await client.createSchema({
813
- * fields: [{ name: 'newField', type: 'string' }],
814
- * mode: 'additive'
815
- * });
816
- *
817
- * @example
818
- * // Replace entire schema (destructive, clears all documents)
819
- * const schema = await client.createSchema({
820
- * fields: [...],
821
- * mode: 'replace',
822
- * confirmDelete: true
823
- * });
824
- */
825
- async createSchema(request) {
826
- this.logger.verbose('Creating/updating schema', {
827
- fieldCount: request.fields.length,
828
- mode: request.mode || 'additive'
829
- });
830
- if (!this.writeSecret) {
831
- this.logger.error('Write secret required but not provided', {
832
- operation: 'createSchema',
833
- storeId: this.storeId
834
- });
835
- throw new Error('Write secret is required for createSchema');
836
- }
837
- try {
838
- const apiRequest = {
839
- fields: request.fields.map(f => ({
840
- name: f.name,
841
- type: f.type,
842
- facet: f.facet,
843
- index: f.index,
844
- optional: f.optional,
845
- sort: f.sort,
846
- infix: f.infix,
847
- locale: f.locale,
848
- })),
849
- mode: request.mode,
850
- confirm_delete: request.confirmDelete,
851
- default_sorting_field: request.defaultSortingField,
852
- enable_nested_fields: request.enableNestedFields,
853
- };
854
- const response = await this.schemaApi.apiV1StoresXStoreIDSchemaPost(this.storeId, this.writeSecret, this.storeId, apiRequest);
855
- const responseData = response.data;
856
- this.logger.info('Schema created/updated successfully', {
857
- name: responseData.data?.name,
858
- fieldCount: responseData.data?.fields?.length
859
- });
860
- return {
861
- name: responseData.data?.name || '',
862
- fields: (responseData.data?.fields || []).map(f => ({
863
- name: f.name || '',
864
- type: f.type || 'string',
865
- facet: f.facet,
866
- index: f.index,
867
- optional: f.optional,
868
- sort: f.sort,
869
- infix: f.infix,
870
- locale: f.locale,
871
- })),
872
- defaultSortingField: responseData.data?.default_sorting_field,
873
- numDocuments: responseData.data?.num_documents || 0,
874
- createdAt: responseData.data?.created_at,
875
- };
876
- }
877
- catch (error) {
878
- this.logger.error('Failed to create/update schema', { error: error.message });
879
- throw this.handleError(error, 'createSchema');
880
- }
881
- }
882
- /**
883
- * Get the current collection schema for this store
884
- *
885
- * @returns SchemaResponse with the current schema
886
- *
887
- * @example
888
- * const schema = await client.getSchema();
889
- * console.log('Fields:', schema.fields.map(f => f.name));
890
- * console.log('Documents:', schema.numDocuments);
891
- */
892
- async getSchema() {
893
- this.logger.verbose('Getting schema', { storeId: this.storeId });
894
- try {
895
- const response = await this.schemaApi.apiV1StoresXStoreIDSchemaGet(this.storeId, this.readSecret, this.storeId);
896
- const responseData = response.data;
897
- this.logger.info('Schema retrieved successfully', {
898
- name: responseData.data?.name,
899
- fieldCount: responseData.data?.fields?.length,
900
- numDocuments: responseData.data?.num_documents
901
- });
902
- return {
903
- name: responseData.data?.name || '',
904
- fields: (responseData.data?.fields || []).map(f => ({
905
- name: f.name || '',
906
- type: f.type || 'string',
907
- facet: f.facet,
908
- index: f.index,
909
- optional: f.optional,
910
- sort: f.sort,
911
- infix: f.infix,
912
- locale: f.locale,
913
- })),
914
- defaultSortingField: responseData.data?.default_sorting_field,
915
- numDocuments: responseData.data?.num_documents || 0,
916
- createdAt: responseData.data?.created_at,
917
- };
918
- }
919
- catch (error) {
920
- this.logger.error('Failed to get schema', { error: error.message });
921
- throw this.handleError(error, 'getSchema');
922
- }
923
- }
924
- /**
925
- * Clear all documents from the store's collection
926
- *
927
- * This deletes all documents but preserves the collection and its schema.
928
- * Useful for re-indexing data from scratch.
929
- *
930
- * @returns ClearDocumentsResponse with count of deleted documents
931
- *
932
- * @example
933
- * const result = await client.clearDocuments();
934
- * console.log(`Cleared ${result.deletedCount} documents`);
935
- */
936
- async clearDocuments() {
937
- this.logger.verbose('Clearing all documents', { storeId: this.storeId });
938
- if (!this.writeSecret) {
939
- this.logger.error('Write secret required but not provided', {
940
- operation: 'clearDocuments',
941
- storeId: this.storeId
942
- });
943
- throw new Error('Write secret is required for clearDocuments');
944
- }
945
- try {
946
- const response = await this.schemaApi.apiV1StoresXStoreIDDocumentsDelete(this.storeId, this.writeSecret, this.storeId);
947
- const responseData = response.data;
948
- this.logger.info('Documents cleared successfully', {
949
- deletedCount: responseData.data?.deleted_count
950
- });
951
- return {
952
- deletedCount: responseData.data?.deleted_count || 0,
953
- message: responseData.data?.message || 'Documents cleared',
954
- };
955
- }
956
- catch (error) {
957
- this.logger.error('Failed to clear documents', { error: error.message });
958
- throw this.handleError(error, 'clearDocuments');
959
- }
960
- }
961
- /**
962
- * Asynchronously collect browser context (called during initialization)
963
- */
964
- async collectContextAsync() {
965
- try {
966
- this.cachedBrowserContext = await this.contextCollector.collect();
967
- this.logger.verbose('Browser context collected', {
968
- hasFingerprint: !!this.cachedBrowserContext?.device_fingerprint,
969
- platform: this.cachedBrowserContext?.platform,
970
- timezone: this.cachedBrowserContext?.timezone,
971
- });
972
- }
973
- catch (error) {
974
- this.logger.warn('Failed to collect browser context', { error: error.message });
975
- }
976
- }
977
- /**
978
- * Create the event sender function for the event queue
979
- */
980
- createQueueSender() {
981
- return async (events) => {
982
- if (events.length === 1) {
983
- // Send single event
984
- await this.sendEventDirect(events[0]);
985
- }
986
- else {
987
- // Send as batch
988
- await this.sendEventsBatchDirect(events);
989
- }
990
- };
991
- }
992
- /**
993
- * Send a single event directly to the backend (bypasses queue)
994
- */
995
- async sendEventDirect(payload) {
996
- const response = await this.analyticsApi.analyticsEventPost(this.storeId, this.readSecret, payload, {
997
- headers: {
998
- 'x-storeid': this.storeId,
999
- 'x-storesecret': this.readSecret,
1000
- }
1001
- });
1002
- if (response.status >= 400) {
1003
- throw new Error(`Failed to send event: ${response.status}`);
1004
- }
1005
- }
1006
- /**
1007
- * Send a batch of events directly to the backend (bypasses queue)
1008
- */
1009
- async sendEventsBatchDirect(payloads) {
1010
- const batchRequest = {
1011
- events: payloads
1012
- };
1013
- const response = await this.analyticsApi.analyticsBatchPost(this.storeId, this.readSecret, batchRequest, {
1014
- headers: {
1015
- 'x-storeid': this.storeId,
1016
- 'x-storesecret': this.readSecret,
1017
- }
1018
- });
1019
- if (response.status >= 400) {
1020
- throw new Error(`Failed to send batch events: ${response.status}`);
1021
- }
1022
- }
1023
- /**
1024
- * Get browser context (sync if cached, otherwise returns null)
1025
- * Use collectBrowserContext() for async collection
1026
- */
1027
- getBrowserContext() {
1028
- return this.cachedBrowserContext;
1029
- }
1030
- /**
1031
- * Collect browser context asynchronously
1032
- */
1033
- async collectBrowserContext() {
1034
- this.cachedBrowserContext = await this.contextCollector.collect();
1035
- return this.cachedBrowserContext;
1036
- }
1037
- /**
1038
- * Build event payload with identifiers and browser context
1039
- * Ensures user_id or anon_id is present, and sets correlation_id/search_id at top level
1040
- */
1041
- buildEventPayload(event, context) {
1042
- const payload = {
1043
- ...event,
1044
- // Set identifiers from context or fall back to client defaults
1045
- user_id: event.user_id || context?.userId || this.userId,
1046
- anon_id: event.anon_id || context?.anonId || this.anonId,
1047
- session_id: event.session_id || context?.sessionId || this.sessionId,
1048
- // Set correlation_id at top level if provided (per identifier spec)
1049
- correlation_id: event.correlation_id || context?.correlationId,
1050
- // Set search_id at top level (not just in metadata) if provided (per identifier spec)
1051
- search_id: event.search_id || context?.searchId,
1052
- };
1053
- // Ensure either user_id or anon_id is present
1054
- if (!payload.user_id && !payload.anon_id) {
1055
- // This should not happen as we always have anonId, but add check for safety
1056
- payload.anon_id = this.anonId;
1057
- }
1058
- // Also include search_id in metadata for backward compatibility
1059
- if (payload.search_id && !payload.metadata) {
1060
- payload.metadata = {};
1061
- }
1062
- if (payload.search_id && payload.metadata && !payload.metadata.search_id) {
1063
- payload.metadata.search_id = payload.search_id;
1064
- }
1065
- // Add browser context if enabled and available
1066
- if (this.enableContextCollection && this.cachedBrowserContext) {
1067
- const ctx = this.cachedBrowserContext;
1068
- // Screen/viewport dimensions
1069
- payload.screen_width = ctx.screen_width;
1070
- payload.screen_height = ctx.screen_height;
1071
- payload.viewport_width = ctx.viewport_width;
1072
- payload.viewport_height = ctx.viewport_height;
1073
- payload.color_depth = ctx.color_depth;
1074
- payload.pixel_ratio = ctx.pixel_ratio;
1075
- // Device fingerprint (if available)
1076
- if (ctx.device_fingerprint) {
1077
- payload.device_fingerprint = ctx.device_fingerprint;
1078
- }
1079
- // Browser information
1080
- payload.browser_name = ctx.browser_name;
1081
- payload.browser_version = ctx.browser_version;
1082
- payload.browser_language = ctx.browser_language;
1083
- payload.timezone = ctx.timezone;
1084
- payload.timezone_offset = ctx.timezone_offset;
1085
- // Page context
1086
- payload.page_url = ctx.page_url;
1087
- payload.page_path = ctx.page_path;
1088
- payload.page_title = ctx.page_title;
1089
- payload.page_referrer = ctx.page_referrer;
1090
- // UTM parameters (if present)
1091
- if (ctx.utm_source)
1092
- payload.utm_source = ctx.utm_source;
1093
- if (ctx.utm_medium)
1094
- payload.utm_medium = ctx.utm_medium;
1095
- if (ctx.utm_campaign)
1096
- payload.utm_campaign = ctx.utm_campaign;
1097
- if (ctx.utm_term)
1098
- payload.utm_term = ctx.utm_term;
1099
- if (ctx.utm_content)
1100
- payload.utm_content = ctx.utm_content;
1101
- // Connection info (if available)
1102
- if (ctx.connection_type)
1103
- payload.connection_type = ctx.connection_type;
1104
- if (ctx.connection_effective_type)
1105
- payload.connection_effective_type = ctx.connection_effective_type;
1106
- // Platform info
1107
- payload.platform = ctx.platform;
1108
- payload.is_mobile = ctx.is_mobile;
1109
- payload.is_tablet = ctx.is_tablet;
1110
- payload.is_touch_device = ctx.is_touch_device;
1111
- }
1112
- return payload;
1113
- }
1114
- /**
1115
- * Track a single analytics event
1116
- *
1117
- * @param event - Event payload (partial, will be enriched with identifiers)
1118
- * @param context - Optional search context for linking events to searches
1119
- */
1120
- async trackEvent(event, context) {
1121
- this.logger.verbose('Tracking analytics event', {
1122
- eventName: event.event_name,
1123
- hasContext: !!context
1124
- });
1125
- const payload = this.buildEventPayload(event, context);
1126
- // Use event queue if enabled
1127
- if (this.enableEventQueue && this.eventQueue) {
1128
- this.eventQueue.enqueue(payload);
1129
- this.logger.verbose('Event queued for sending', {
1130
- eventName: event.event_name,
1131
- queueSize: this.eventQueue.size()
1132
- });
1133
- return;
1134
- }
1135
- try {
1136
- this.logger.verbose('Sending analytics event', {
1137
- endpoint: '/api/analytics/event',
1138
- eventName: event.event_name
1139
- });
1140
- // Cast to DataTypesEventPayload for API call (backend accepts extended fields)
1141
- const response = await this.analyticsApi.analyticsEventPost(this.storeId, this.readSecret, payload, // Type assertion needed as generated types may not include correlation_id/search_id yet
1142
- {
1143
- headers: {
1144
- 'x-storeid': this.storeId,
1145
- 'x-storesecret': this.readSecret,
1146
- }
1147
- });
1148
- this.logger.verbose('Analytics event tracked successfully', {
1149
- eventName: event.event_name,
1150
- status: response.status
1151
- });
1152
- }
1153
- catch (error) {
1154
- this.logger.error('Failed to track event', { eventName: event.event_name, error: error.message });
1155
- throw this.handleError(error, 'trackEvent');
1156
- }
1157
- }
1158
- /**
1159
- * Track multiple analytics events (batch)
1160
- *
1161
- * Note: Events should already have identifiers set. Consider using individual trackEvent()
1162
- * calls or buildEventPayload() to ensure identifiers are properly set.
1163
- *
1164
- * @param events - Array of event payloads (should have identifiers already set)
1165
- */
1166
- async trackEvents(events, context) {
1167
- this.logger.verbose('Tracking batch analytics events', {
1168
- count: events.length,
1169
- hasContext: !!context
1170
- });
1171
- if (events.length > 100) {
1172
- this.logger.error('Batch size exceeds maximum', { count: events.length, max: 100 });
1173
- throw new Error('Maximum 100 events per batch');
1174
- }
1175
- // Build full event payloads with identifiers
1176
- const enrichedEvents = events.map(event => this.buildEventPayload(event, context));
1177
- // Use event queue if enabled
1178
- if (this.enableEventQueue && this.eventQueue) {
1179
- this.eventQueue.enqueueBatch(enrichedEvents);
1180
- this.logger.verbose('Events queued for sending', {
1181
- count: events.length,
1182
- queueSize: this.eventQueue.size()
1183
- });
1184
- return;
1185
- }
1186
- try {
1187
- this.logger.verbose('Sending batch analytics events', {
1188
- endpoint: '/api/analytics/batch',
1189
- eventCount: events.length
1190
- });
1191
- const batchRequest = {
1192
- events: enrichedEvents // Type assertion for extended payloads
1193
- };
1194
- const response = await this.analyticsApi.analyticsBatchPost(this.storeId, this.readSecret, batchRequest, {
1195
- headers: {
1196
- 'x-storeid': this.storeId,
1197
- 'x-storesecret': this.readSecret,
1198
- }
1199
- });
1200
- this.logger.info('Batch analytics events tracked successfully', {
1201
- count: events.length,
1202
- status: response.status
1203
- });
1204
- }
1205
- catch (error) {
1206
- this.logger.error('Failed to track batch events', { count: events.length, error: error.message });
1207
- throw this.handleError(error, 'trackEvents');
1208
- }
1209
- }
1210
- /**
1211
- * Track a search event
1212
- *
1213
- * @param params - Search tracking parameters
1214
- */
1215
- async trackSearch(params) {
1216
- this.logger.verbose('Tracking search event', {
1217
- query: params.query,
1218
- resultsCount: params.resultsCount,
1219
- hasContext: !!params.context
1220
- });
1221
- await this.trackEvent({
1222
- event_name: 'search.performed',
1223
- query: params.query,
1224
- results_count: params.resultsCount,
1225
- analytics_tags: params.analyticsTags,
1226
- metadata: params.metadata,
1227
- }, params.context);
1228
- }
1229
- /**
1230
- * Track an impression event
1231
- *
1232
- * @param params - Impression tracking parameters
1233
- */
1234
- async trackImpression(params) {
1235
- this.logger.verbose('Tracking impression event', {
1236
- itemId: params.itemId,
1237
- position: params.position,
1238
- hasContext: !!params.context
1239
- });
1240
- await this.trackEvent({
1241
- event_name: 'impression',
1242
- clicked_item_id: params.itemId,
1243
- metadata: {
1244
- position: params.position,
1245
- list_type: params.listType,
1246
- search_query: params.searchQuery,
1247
- ...params.metadata,
1248
- },
1249
- }, params.context);
1250
- }
1251
- async trackClick(itemId, position, contextOrSearchId) {
1252
- this.logger.verbose('Tracking click event', {
1253
- itemId,
1254
- position,
1255
- hasContext: !!contextOrSearchId
1256
- });
1257
- // Handle backward compatibility: if third param is string, treat as searchId
1258
- const context = typeof contextOrSearchId === 'string'
1259
- ? { correlationId: '', searchId: contextOrSearchId, anonId: this.anonId, sessionId: this.sessionId }
1260
- : contextOrSearchId;
1261
- await this.trackEvent({
1262
- event_name: 'product_click',
1263
- clicked_item_id: itemId,
1264
- metadata: {
1265
- position,
1266
- },
1267
- }, context);
1268
- }
1269
- async trackView(itemId, contextOrSearchId) {
1270
- this.logger.verbose('Tracking view event', {
1271
- itemId,
1272
- hasContext: !!contextOrSearchId
1273
- });
1274
- // Handle backward compatibility: if second param is string, treat as searchId
1275
- const context = typeof contextOrSearchId === 'string'
1276
- ? { correlationId: '', searchId: contextOrSearchId, anonId: this.anonId, sessionId: this.sessionId }
1277
- : contextOrSearchId;
1278
- await this.trackEvent({
1279
- event_name: 'view',
1280
- clicked_item_id: itemId,
1281
- }, context);
1282
- }
1283
- async trackConversion(itemId, value, currency, contextOrSearchId) {
1284
- this.logger.verbose('Tracking conversion event', {
1285
- itemId,
1286
- value,
1287
- currency,
1288
- hasContext: !!contextOrSearchId
1289
- });
1290
- // Handle backward compatibility: if fourth param is string, treat as searchId
1291
- const context = typeof contextOrSearchId === 'string'
1292
- ? { correlationId: '', searchId: contextOrSearchId, anonId: this.anonId, sessionId: this.sessionId }
1293
- : contextOrSearchId;
1294
- await this.trackEvent({
1295
- event_name: 'conversion',
1296
- clicked_item_id: itemId,
1297
- value,
1298
- currency,
1299
- }, context);
1300
- }
1301
- /**
1302
- * Track a custom event
1303
- *
1304
- * @param eventName - Custom event name
1305
- * @param payload - Additional event data
1306
- * @param context - Optional search context for linking events to searches
1307
- */
1308
- async trackCustom(eventName, payload = {}, context) {
1309
- this.logger.verbose('Tracking custom event', {
1310
- eventName,
1311
- hasContext: !!context
1312
- });
1313
- await this.trackEvent({
1314
- ...payload,
1315
- event_name: eventName,
1316
- }, context);
1317
- }
1318
- /**
1319
- * Validate an event payload before sending
1320
- * Useful for debugging and ensuring events are properly formatted
1321
- *
1322
- * @param event - Event payload to validate
1323
- * @returns Validation result with success status and any error messages
1324
- */
1325
- async validateEvent(event) {
1326
- this.logger.verbose('Validating event payload', { eventName: event.event_name });
1327
- try {
1328
- this.logger.verbose('Validating event payload', {
1329
- endpoint: '/api/analytics/validate',
1330
- eventName: event.event_name
1331
- });
1332
- const payload = this.buildEventPayload(event);
1333
- const response = await this.analyticsApi.analyticsValidatePost(this.storeId, this.readSecret, payload, {
1334
- headers: {
1335
- 'x-storeid': this.storeId,
1336
- 'x-storesecret': this.readSecret,
1337
- }
1338
- });
1339
- const result = response.data?.data || response.data;
1340
- const isValid = result?.valid !== false && response.status === 200;
1341
- this.logger.verbose('Event validation result', {
1342
- valid: isValid,
1343
- eventName: event.event_name,
1344
- message: result?.message
1345
- });
1346
- return {
1347
- valid: isValid,
1348
- message: result?.message || (isValid ? 'Event is valid' : 'Event validation failed'),
1349
- errors: result?.errors,
1350
- };
1351
- }
1352
- catch (error) {
1353
- this.logger.error('Event validation failed', { error: error.message });
1354
- return {
1355
- valid: false,
1356
- message: error.response?.data?.message || error.message || 'Validation request failed',
1357
- errors: error.response?.data?.errors || error.response?.data,
1358
- };
1359
- }
1360
- }
1361
- /**
1362
- * Get event schema
1363
- * Returns the JSON schema for event payloads, useful for understanding required/optional fields
1364
- *
1365
- * @returns Event schema with field definitions, types, and validation rules
1366
- */
1367
- async getEventSchema() {
1368
- this.logger.verbose('Getting event schema');
1369
- try {
1370
- const response = await this.analyticsApi.analyticsSchemaGet(this.storeId, this.readSecret, {
1371
- headers: {
1372
- 'x-storeid': this.storeId,
1373
- 'x-storesecret': this.readSecret,
1374
- }
1375
- });
1376
- const schema = response.data?.data || response.data;
1377
- this.logger.verbose('Event schema retrieved', { schemaKeys: schema ? Object.keys(schema) : [] });
1378
- return schema;
1379
- }
1380
- catch (error) {
1381
- this.logger.error('Failed to get event schema', { error: error.message });
1382
- throw this.handleError(error, 'getEventSchema');
1383
- }
1384
- }
1385
- /**
1386
- * Set user ID (call this when user logs in)
1387
- *
1388
- * @param userId - Authenticated user ID
1389
- */
1390
- setUserId(userId) {
1391
- this.userId = userId;
1392
- this.logger.info('User ID updated', { userId });
1393
- }
1394
- /**
1395
- * Clear user ID (call this when user logs out)
1396
- */
1397
- clearUserId() {
1398
- this.userId = undefined;
1399
- this.logger.info('User ID cleared');
1400
- }
1401
- /**
1402
- * Get current identifiers
1403
- * Useful for debugging or custom event tracking
1404
- */
1405
- getIdentifiers() {
1406
- return {
1407
- userId: this.userId,
1408
- anonId: this.anonId,
1409
- sessionId: this.sessionId,
1410
- };
1411
- }
1412
- /**
1413
- * Get the event queue instance (if enabled)
1414
- */
1415
- getEventQueue() {
1416
- return this.eventQueue;
1417
- }
1418
- /**
1419
- * Flush the event queue manually
1420
- * Useful when you want to ensure events are sent before page unload
1421
- */
1422
- async flushEventQueue() {
1423
- if (this.eventQueue) {
1424
- await this.eventQueue.flush();
1425
- }
1426
- }
1427
- /**
1428
- * Get event queue statistics
1429
- */
1430
- getEventQueueStats() {
1431
- if (!this.eventQueue) {
1432
- return null;
1433
- }
1434
- return {
1435
- enabled: this.enableEventQueue,
1436
- ...this.eventQueue.getStats()
1437
- };
1438
- }
1439
- /**
1440
- * Identify a user and link their anonymous ID/fingerprint to their user ID
1441
- * Call this when a user logs in to enable cross-device/session tracking
1442
- *
1443
- * @param userId - The authenticated user ID
1444
- * @param traits - Optional user traits/properties
1445
- */
1446
- async identify(userId, traits) {
1447
- this.logger.verbose('Identifying user', { userId });
1448
- // Update internal user ID
1449
- this.userId = userId;
1450
- // Collect current context for fingerprint
1451
- const context = this.cachedBrowserContext || await this.collectBrowserContext();
1452
- const payload = {
1453
- user_id: userId,
1454
- anon_id: this.anonId,
1455
- session_id: this.sessionId,
1456
- device_fingerprint: context?.device_fingerprint,
1457
- traits,
1458
- };
1459
- try {
1460
- const response = await axios_1.default.post(`${this.config.basePath}/api/analytics/identify`, payload, {
1461
- headers: {
1462
- 'x-storeid': this.storeId,
1463
- 'x-storesecret': this.readSecret,
1464
- },
1465
- timeout: 10000,
1466
- });
1467
- this.logger.info('User identified successfully', {
1468
- userId,
1469
- linked: {
1470
- anonId: !!this.anonId,
1471
- fingerprint: !!context?.device_fingerprint,
1472
- }
1473
- });
1474
- }
1475
- catch (error) {
1476
- this.logger.error('Failed to identify user', { userId, error: error.message });
1477
- // Don't throw - identify failures shouldn't break the app
1478
- }
1479
- }
1480
- /**
1481
- * Alias an anonymous ID to a user ID
1482
- * Use this when you want to link an external anonymous ID to a user
1483
- *
1484
- * @param anonId - The anonymous ID to alias
1485
- * @param userId - The user ID to link to
1486
- */
1487
- async alias(anonId, userId) {
1488
- this.logger.verbose('Creating alias', { anonId, userId });
1489
- const payload = {
1490
- user_id: userId,
1491
- anon_id: anonId,
1492
- };
1493
- try {
1494
- await axios_1.default.post(`${this.config.basePath}/api/analytics/identify`, payload, {
1495
- headers: {
1496
- 'x-storeid': this.storeId,
1497
- 'x-storesecret': this.readSecret,
1498
- },
1499
- timeout: 10000,
1500
- });
1501
- this.logger.info('Alias created successfully', { anonId, userId });
1502
- }
1503
- catch (error) {
1504
- this.logger.error('Failed to create alias', { anonId, userId, error: error.message });
1505
- }
1506
- }
1507
- /**
1508
- * Handle errors consistently
1509
- */
1510
- handleError(error, operation) {
1511
- if (error.response) {
1512
- const status = error.response.status;
1513
- const data = error.response.data;
1514
- const message = data?.message || `API request failed with status ${status}`;
1515
- // Log based on status code
1516
- if (status >= 500) {
1517
- this.logger.error('API server error', { operation, status, message });
1518
- }
1519
- else if (status === 401 || status === 403) {
1520
- this.logger.error('Authentication error', { operation, status, message });
1521
- }
1522
- else if (status === 429) {
1523
- this.logger.warn('Rate limit exceeded', { operation, status, message });
1524
- }
1525
- else {
1526
- this.logger.error('API request failed', { operation, status, message });
1527
- }
1528
- return new Error(`[${operation}] ${message} (${status})`);
1529
- }
1530
- else if (error.request) {
1531
- this.logger.error('Network error', { operation, error: error.message });
1532
- return new Error(`[${operation}] Network error: ${error.message}`);
1533
- }
1534
- else {
1535
- this.logger.error('Request error', { operation, error: error.message });
1536
- return new Error(`[${operation}] ${error.message}`);
1537
- }
1538
- }
1539
- /**
1540
- * Get the logger instance
1541
- */
1542
- getLogger() {
1543
- return this.logger;
1544
- }
1545
- }
1546
- exports.SeekoraClient = SeekoraClient;
1547
- // Export default
1548
- exports.default = SeekoraClient;