@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/src/client.js
DELETED
|
@@ -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;
|