@seekora-ai/search-sdk 1.0.0
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/README.md +213 -0
- package/cdn/.htaccess +37 -0
- package/cdn/README.md +130 -0
- package/cdn/_headers +25 -0
- package/cdn/example.html +299 -0
- package/cdn/seekora-sdk.js +4651 -0
- package/cdn/seekora-sdk.min.js +17 -0
- package/cdn/ui-sdk-integration-example.html +183 -0
- package/dist/cdn.d.ts +16 -0
- package/dist/cdn.js +26 -0
- package/dist/client.d.ts +401 -0
- package/dist/client.js +991 -0
- package/dist/config-loader.d.ts +43 -0
- package/dist/config-loader.js +147 -0
- package/dist/config.d.ts +22 -0
- package/dist/config.js +58 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.js +40 -0
- package/dist/logger.d.ts +61 -0
- package/dist/logger.js +172 -0
- package/dist/src/cdn.d.ts +16 -0
- package/dist/src/cdn.js +26 -0
- package/dist/src/client.d.ts +401 -0
- package/dist/src/client.js +991 -0
- package/dist/src/config-loader.d.ts +43 -0
- package/dist/src/config-loader.js +147 -0
- package/dist/src/config.d.ts +22 -0
- package/dist/src/config.js +58 -0
- package/dist/src/index.d.ts +12 -0
- package/dist/src/index.js +40 -0
- package/dist/src/logger.d.ts +61 -0
- package/dist/src/logger.js +172 -0
- package/dist/src/utils.d.ts +20 -0
- package/dist/src/utils.js +73 -0
- package/dist/utils.d.ts +20 -0
- package/dist/utils.js +73 -0
- package/package.json +47 -0
package/dist/client.js
ADDED
|
@@ -0,0 +1,991 @@
|
|
|
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
|
+
/**
|
|
18
|
+
* Seekora SDK Client
|
|
19
|
+
*
|
|
20
|
+
* Provides a clean, easy-to-use interface for the Seekora Search API
|
|
21
|
+
*/
|
|
22
|
+
class SeekoraClient {
|
|
23
|
+
constructor(config = {}) {
|
|
24
|
+
// Load configuration from file, env, and code (in that order)
|
|
25
|
+
const mergedConfig = (0, config_loader_1.loadConfig)(config);
|
|
26
|
+
this.storeId = mergedConfig.storeId;
|
|
27
|
+
this.readSecret = mergedConfig.readSecret;
|
|
28
|
+
this.writeSecret = mergedConfig.writeSecret;
|
|
29
|
+
// Initialize logger FIRST (before using it)
|
|
30
|
+
const logLevel = mergedConfig.logLevel || config.logLevel || (0, logger_1.getLogLevelFromEnv)();
|
|
31
|
+
this.logger = (0, logger_1.createLogger)({
|
|
32
|
+
level: logLevel,
|
|
33
|
+
...config.logger,
|
|
34
|
+
});
|
|
35
|
+
// Initialize identifiers
|
|
36
|
+
this.userId = config.userId;
|
|
37
|
+
this.anonId = config.anonId || (0, utils_1.getOrCreateAnonId)();
|
|
38
|
+
this.sessionId = config.sessionId || (0, utils_1.getOrCreateSessionId)();
|
|
39
|
+
this.autoTrackSearch = config.autoTrackSearch || false;
|
|
40
|
+
// Log identifier initialization
|
|
41
|
+
this.logger.verbose('Client identifiers initialized', {
|
|
42
|
+
hasUserId: !!this.userId,
|
|
43
|
+
anonId: this.anonId.substring(0, 8) + '...', // Log partial ID for privacy
|
|
44
|
+
sessionId: this.sessionId.substring(0, 8) + '...',
|
|
45
|
+
autoTrackSearch: this.autoTrackSearch
|
|
46
|
+
});
|
|
47
|
+
this.logger.verbose('Initializing SeekoraClient', {
|
|
48
|
+
storeId: this.storeId,
|
|
49
|
+
environment: mergedConfig.environment,
|
|
50
|
+
logLevel,
|
|
51
|
+
});
|
|
52
|
+
// Get base URL from environment or config
|
|
53
|
+
const baseUrl = (0, config_1.getBaseUrl)(mergedConfig.environment, mergedConfig.baseUrl);
|
|
54
|
+
this.logger.verbose('Using base URL', { baseUrl });
|
|
55
|
+
// Create configuration with base URL
|
|
56
|
+
this.config = new generated_1.Configuration({
|
|
57
|
+
basePath: baseUrl,
|
|
58
|
+
baseOptions: {
|
|
59
|
+
timeout: mergedConfig.timeout || 30000,
|
|
60
|
+
headers: {
|
|
61
|
+
'x-storeid': this.storeId,
|
|
62
|
+
'x-storesecret': this.readSecret,
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
// Initialize API clients
|
|
67
|
+
this.searchApi = new generated_1.SearchApi(this.config);
|
|
68
|
+
this.suggestionsApi = new generated_1.QuerySuggestionsApi(this.config);
|
|
69
|
+
this.suggestionsConfigApi = new generated_1.SDKQuerySuggestionsConfigApi(this.config);
|
|
70
|
+
this.analyticsApi = new generated_1.AnalyticsEventsApi(this.config);
|
|
71
|
+
this.storesApi = new generated_1.SDKStoreConfigApi(this.config);
|
|
72
|
+
this.logger.info('SeekoraClient initialized successfully');
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Search for documents
|
|
76
|
+
*
|
|
77
|
+
* Generates a new correlation_id for this search and returns SearchContext
|
|
78
|
+
* for linking subsequent events (clicks, conversions) to this search.
|
|
79
|
+
*
|
|
80
|
+
* @param query - Search query string
|
|
81
|
+
* @param options - Search options (pagination, filters, facets, etc.)
|
|
82
|
+
* @returns SearchResponse with results and SearchContext
|
|
83
|
+
*/
|
|
84
|
+
async search(query, options) {
|
|
85
|
+
// Convert empty query to wildcard
|
|
86
|
+
const searchQuery = query.trim() || '*';
|
|
87
|
+
// Generate correlation_id for this search journey
|
|
88
|
+
const correlationId = (0, utils_1.generateUUID)();
|
|
89
|
+
this.logger.verbose('Executing search', {
|
|
90
|
+
originalQuery: query,
|
|
91
|
+
searchQuery,
|
|
92
|
+
correlationId,
|
|
93
|
+
options
|
|
94
|
+
});
|
|
95
|
+
const searchRequest = {
|
|
96
|
+
q: searchQuery,
|
|
97
|
+
per_page: options?.per_page || 10,
|
|
98
|
+
page: options?.page || 1,
|
|
99
|
+
filter: options?.filter || options?.filter_by,
|
|
100
|
+
facet_by: options?.facet_by,
|
|
101
|
+
sort: options?.sort || options?.sort_by,
|
|
102
|
+
analytics_tags: options?.analytics_tags,
|
|
103
|
+
widget_mode: options?.widget_mode || false,
|
|
104
|
+
// New advanced parameters
|
|
105
|
+
search_fields: options?.search_fields,
|
|
106
|
+
field_weights: options?.field_weights,
|
|
107
|
+
return_fields: options?.return_fields,
|
|
108
|
+
omit_fields: options?.omit_fields,
|
|
109
|
+
snippet_fields: options?.snippet_fields,
|
|
110
|
+
full_snippet_fields: options?.full_snippet_fields,
|
|
111
|
+
snippet_prefix: options?.snippet_prefix,
|
|
112
|
+
snippet_suffix: options?.snippet_suffix,
|
|
113
|
+
snippet_token_limit: options?.snippet_token_limit,
|
|
114
|
+
snippet_min_len: options?.snippet_min_len,
|
|
115
|
+
include_snippets: options?.include_snippets,
|
|
116
|
+
group_field: options?.group_field,
|
|
117
|
+
group_size: options?.group_size,
|
|
118
|
+
prefix_mode: options?.prefix_mode,
|
|
119
|
+
infix_mode: options?.infix_mode,
|
|
120
|
+
typo_max: options?.typo_max,
|
|
121
|
+
typo_min_len_1: options?.typo_min_len_1,
|
|
122
|
+
typo_min_len_2: options?.typo_min_len_2,
|
|
123
|
+
search_timeout_ms: options?.search_timeout_ms,
|
|
124
|
+
require_all_terms: options?.require_all_terms,
|
|
125
|
+
exact_match_boost: options?.exact_match_boost,
|
|
126
|
+
cache_results: options?.cache_results,
|
|
127
|
+
apply_rules: options?.apply_rules,
|
|
128
|
+
preset_name: options?.preset_name,
|
|
129
|
+
facet_search_text: options?.facet_search_text,
|
|
130
|
+
include_suggestions: options?.include_suggestions,
|
|
131
|
+
suggestions_limit: options?.suggestions_limit,
|
|
132
|
+
max_facet_values: options?.max_facet_values,
|
|
133
|
+
stopword_sets: options?.stopword_sets,
|
|
134
|
+
synonym_sets: options?.synonym_sets,
|
|
135
|
+
};
|
|
136
|
+
// Log search request details (verbose level for debugging)
|
|
137
|
+
this.logger.verbose('Search request prepared', {
|
|
138
|
+
filter: searchRequest.filter,
|
|
139
|
+
filterType: typeof searchRequest.filter,
|
|
140
|
+
fullRequest: searchRequest
|
|
141
|
+
});
|
|
142
|
+
try {
|
|
143
|
+
// Log API request start
|
|
144
|
+
this.logger.verbose('Sending search API request', {
|
|
145
|
+
endpoint: '/api/v1/search',
|
|
146
|
+
method: 'POST',
|
|
147
|
+
storeId: this.storeId
|
|
148
|
+
});
|
|
149
|
+
// Build headers with personalization support
|
|
150
|
+
const headers = {
|
|
151
|
+
'x-storeid': this.storeId,
|
|
152
|
+
'x-storesecret': this.readSecret,
|
|
153
|
+
};
|
|
154
|
+
// Add personalization headers if available
|
|
155
|
+
if (this.userId) {
|
|
156
|
+
headers['x-user-id'] = this.userId;
|
|
157
|
+
}
|
|
158
|
+
if (this.anonId) {
|
|
159
|
+
headers['x-anon-id'] = this.anonId;
|
|
160
|
+
}
|
|
161
|
+
if (this.sessionId) {
|
|
162
|
+
headers['x-session-id'] = this.sessionId;
|
|
163
|
+
}
|
|
164
|
+
const response = await this.searchApi.v1SearchPost(this.storeId, this.readSecret, searchRequest, this.userId, this.anonId, this.sessionId, {
|
|
165
|
+
headers
|
|
166
|
+
});
|
|
167
|
+
// Log API response received
|
|
168
|
+
this.logger.verbose('Search API response received', {
|
|
169
|
+
status: response.status,
|
|
170
|
+
hasData: !!response.data
|
|
171
|
+
});
|
|
172
|
+
// Extract search results
|
|
173
|
+
const data = response.data;
|
|
174
|
+
const facets = data?.data?.facets || [];
|
|
175
|
+
// Log API response (verbose level for detailed debugging)
|
|
176
|
+
this.logger.verbose('API response received', {
|
|
177
|
+
status: response.status,
|
|
178
|
+
totalResults: data?.data?.total_results || 0,
|
|
179
|
+
resultsCount: data?.data?.results?.length || 0,
|
|
180
|
+
facetsType: Array.isArray(facets) ? 'array' : typeof facets,
|
|
181
|
+
facetsCount: Array.isArray(facets) ? facets.length : Object.keys(facets || {}).length
|
|
182
|
+
});
|
|
183
|
+
// Convert facets array to a more accessible object format
|
|
184
|
+
const facetsMap = {};
|
|
185
|
+
if (Array.isArray(facets)) {
|
|
186
|
+
this.logger.verbose('Processing facets as array', { facetsCount: facets.length });
|
|
187
|
+
facets.forEach((facet, index) => {
|
|
188
|
+
// Handle different facet formats
|
|
189
|
+
const fieldName = facet?.field_name || facet?.fieldName || facet?.name || `facet_${index}`;
|
|
190
|
+
if (fieldName && (facet?.counts || facet?.values || Array.isArray(facet))) {
|
|
191
|
+
facetsMap[fieldName] = {
|
|
192
|
+
field_name: fieldName,
|
|
193
|
+
counts: facet.counts || facet.values || (Array.isArray(facet) ? facet : []),
|
|
194
|
+
stats: facet.stats || {}
|
|
195
|
+
};
|
|
196
|
+
this.logger.verbose('Processed facet', { fieldName, index, counts: facetsMap[fieldName].counts?.length || 0 });
|
|
197
|
+
}
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
else if (facets && typeof facets === 'object' && !Array.isArray(facets)) {
|
|
201
|
+
this.logger.verbose('Processing facets as object', { facetKeys: Object.keys(facets) });
|
|
202
|
+
// If facets is already an object, use it directly but ensure field_name is set
|
|
203
|
+
Object.keys(facets).forEach((key) => {
|
|
204
|
+
const facetData = facets[key];
|
|
205
|
+
if (facetData) {
|
|
206
|
+
facetsMap[key] = {
|
|
207
|
+
field_name: facetData.field_name || key,
|
|
208
|
+
counts: facetData.counts || facetData.values || [],
|
|
209
|
+
stats: facetData.stats || {}
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
this.logger.verbose('Facets processed', {
|
|
215
|
+
facetFields: Object.keys(facetsMap),
|
|
216
|
+
totalFacetFields: Object.keys(facetsMap).length
|
|
217
|
+
});
|
|
218
|
+
// Extract search_id from response (if present)
|
|
219
|
+
const searchId = data?.data?.search_id;
|
|
220
|
+
// Create search context for linking events
|
|
221
|
+
const context = {
|
|
222
|
+
correlationId,
|
|
223
|
+
searchId,
|
|
224
|
+
userId: this.userId,
|
|
225
|
+
anonId: this.anonId,
|
|
226
|
+
sessionId: this.sessionId,
|
|
227
|
+
};
|
|
228
|
+
const results = {
|
|
229
|
+
results: data?.data?.results || [],
|
|
230
|
+
totalResults: data?.data?.total_results || 0,
|
|
231
|
+
searchId, // Keep for backward compatibility
|
|
232
|
+
context, // New: search context for event linking
|
|
233
|
+
facets: facetsMap,
|
|
234
|
+
facet_counts: facetsMap, // Alias for backward compatibility
|
|
235
|
+
...data
|
|
236
|
+
};
|
|
237
|
+
this.logger.info('Search completed', {
|
|
238
|
+
query: searchQuery,
|
|
239
|
+
originalQuery: query,
|
|
240
|
+
totalResults: results.totalResults,
|
|
241
|
+
searchId: results.searchId,
|
|
242
|
+
correlationId,
|
|
243
|
+
});
|
|
244
|
+
// Automatically track search event if enabled
|
|
245
|
+
if (this.autoTrackSearch) {
|
|
246
|
+
this.trackSearch({
|
|
247
|
+
query: searchQuery,
|
|
248
|
+
resultsCount: results.totalResults,
|
|
249
|
+
context,
|
|
250
|
+
}).catch((err) => {
|
|
251
|
+
// Log but don't fail the search if tracking fails
|
|
252
|
+
this.logger.warn('Failed to auto-track search event', { error: err.message });
|
|
253
|
+
});
|
|
254
|
+
}
|
|
255
|
+
return results;
|
|
256
|
+
}
|
|
257
|
+
catch (error) {
|
|
258
|
+
// Log detailed error information
|
|
259
|
+
const errorDetails = {
|
|
260
|
+
query: searchQuery,
|
|
261
|
+
originalQuery: query,
|
|
262
|
+
error: error.message,
|
|
263
|
+
};
|
|
264
|
+
if (error.response) {
|
|
265
|
+
errorDetails.status = error.response.status;
|
|
266
|
+
errorDetails.responseData = error.response.data;
|
|
267
|
+
errorDetails.requestUrl = error.config?.url;
|
|
268
|
+
errorDetails.requestMethod = error.config?.method;
|
|
269
|
+
}
|
|
270
|
+
else if (error.request) {
|
|
271
|
+
errorDetails.networkError = true;
|
|
272
|
+
errorDetails.requestConfig = {
|
|
273
|
+
url: error.config?.url,
|
|
274
|
+
method: error.config?.method,
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
this.logger.error('Search failed', errorDetails);
|
|
278
|
+
throw this.handleError(error, 'search');
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
/**
|
|
282
|
+
* Get query suggestions
|
|
283
|
+
*
|
|
284
|
+
* Uses POST when filtered_tabs or include_dropdown_recommendations is set (GET does not send include_dropdown_recommendations).
|
|
285
|
+
* With returnFullResponse: true returns full shape including extensions (trending_searches, top_searches, related_searches, popular_brands, filtered_tabs).
|
|
286
|
+
*
|
|
287
|
+
* @param query - Partial query to get suggestions for
|
|
288
|
+
* @param options - Optional parameters for suggestions
|
|
289
|
+
* @returns Suggestion hits array, or QuerySuggestionsFullResponse when returnFullResponse is true
|
|
290
|
+
*/
|
|
291
|
+
async getSuggestions(query, options) {
|
|
292
|
+
this.logger.verbose('Getting query suggestions', { query, options });
|
|
293
|
+
try {
|
|
294
|
+
const headers = {
|
|
295
|
+
'x-storeid': this.storeId,
|
|
296
|
+
'x-storesecret': this.readSecret,
|
|
297
|
+
};
|
|
298
|
+
if (this.userId)
|
|
299
|
+
headers['x-user-id'] = this.userId;
|
|
300
|
+
if (this.anonId)
|
|
301
|
+
headers['x-anon-id'] = this.anonId;
|
|
302
|
+
if (this.sessionId)
|
|
303
|
+
headers['x-session-id'] = this.sessionId;
|
|
304
|
+
const usePost = (options?.filtered_tabs && options.filtered_tabs.length > 0) ||
|
|
305
|
+
options?.include_dropdown_recommendations === true;
|
|
306
|
+
const buildRequestBody = () => {
|
|
307
|
+
const body = {
|
|
308
|
+
query,
|
|
309
|
+
hitsPerPage: options?.hitsPerPage || 5,
|
|
310
|
+
page: options?.page,
|
|
311
|
+
include_categories: options?.include_categories,
|
|
312
|
+
include_facets: options?.include_facets,
|
|
313
|
+
max_categories: options?.max_categories,
|
|
314
|
+
max_facets: options?.max_facets,
|
|
315
|
+
min_popularity: options?.min_popularity,
|
|
316
|
+
time_range: options?.time_range,
|
|
317
|
+
disable_typo_tolerance: options?.disable_typo_tolerance,
|
|
318
|
+
include_dropdown_recommendations: options?.include_dropdown_recommendations,
|
|
319
|
+
filtered_tabs: options?.filtered_tabs,
|
|
320
|
+
};
|
|
321
|
+
if (options?.analytics_tags) {
|
|
322
|
+
body.analytics_tags = Array.isArray(options.analytics_tags)
|
|
323
|
+
? options.analytics_tags
|
|
324
|
+
: [options.analytics_tags];
|
|
325
|
+
}
|
|
326
|
+
if (options?.tags_match_mode)
|
|
327
|
+
body.tags_match_mode = options.tags_match_mode;
|
|
328
|
+
return body;
|
|
329
|
+
};
|
|
330
|
+
if (usePost) {
|
|
331
|
+
this.logger.verbose('Using POST endpoint (filtered_tabs or include_dropdown_recommendations)', {
|
|
332
|
+
endpoint: '/api/v1/suggestions/queries',
|
|
333
|
+
query,
|
|
334
|
+
});
|
|
335
|
+
const requestBody = buildRequestBody();
|
|
336
|
+
const response = await this.suggestionsApi.v1SuggestionsQueriesPost(this.storeId, this.readSecret, this.userId, this.anonId, this.sessionId, requestBody, { headers });
|
|
337
|
+
const responseData = response.data?.data || response.data;
|
|
338
|
+
const suggestions = responseData?.results?.[0]?.hits || responseData?.hits || [];
|
|
339
|
+
const extensions = responseData?.results?.[1]?.extensions ?? responseData?.results?.[0]?.extensions;
|
|
340
|
+
if (options?.returnFullResponse) {
|
|
341
|
+
this.logger.info('Query suggestions retrieved (POST, full response)', {
|
|
342
|
+
query,
|
|
343
|
+
count: suggestions.length,
|
|
344
|
+
status: response.status,
|
|
345
|
+
});
|
|
346
|
+
return {
|
|
347
|
+
suggestions,
|
|
348
|
+
results: responseData?.results,
|
|
349
|
+
extensions: extensions ?? undefined,
|
|
350
|
+
raw: responseData,
|
|
351
|
+
};
|
|
352
|
+
}
|
|
353
|
+
this.logger.info('Query suggestions retrieved (POST)', {
|
|
354
|
+
query,
|
|
355
|
+
count: suggestions.length,
|
|
356
|
+
status: response.status,
|
|
357
|
+
});
|
|
358
|
+
return suggestions;
|
|
359
|
+
}
|
|
360
|
+
// GET for simple requests (no filtered_tabs, no include_dropdown_recommendations)
|
|
361
|
+
this.logger.verbose('Using GET endpoint', {
|
|
362
|
+
endpoint: '/api/v1/suggestions/queries',
|
|
363
|
+
query,
|
|
364
|
+
});
|
|
365
|
+
const analyticsTags = Array.isArray(options?.analytics_tags)
|
|
366
|
+
? options.analytics_tags.join(',')
|
|
367
|
+
: options?.analytics_tags;
|
|
368
|
+
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 });
|
|
369
|
+
const responseData = response.data?.data || response.data;
|
|
370
|
+
const suggestions = responseData?.results?.[0]?.hits || responseData?.hits || [];
|
|
371
|
+
if (options?.returnFullResponse) {
|
|
372
|
+
const extensions = responseData?.results?.[1]?.extensions ?? responseData?.results?.[0]?.extensions;
|
|
373
|
+
this.logger.info('Query suggestions retrieved (GET, full response)', {
|
|
374
|
+
query,
|
|
375
|
+
count: suggestions.length,
|
|
376
|
+
status: response.status,
|
|
377
|
+
});
|
|
378
|
+
return {
|
|
379
|
+
suggestions,
|
|
380
|
+
results: responseData?.results,
|
|
381
|
+
extensions: extensions ?? undefined,
|
|
382
|
+
raw: responseData,
|
|
383
|
+
};
|
|
384
|
+
}
|
|
385
|
+
this.logger.info('Query suggestions retrieved (GET)', {
|
|
386
|
+
query,
|
|
387
|
+
count: suggestions.length,
|
|
388
|
+
status: response.status,
|
|
389
|
+
});
|
|
390
|
+
return suggestions;
|
|
391
|
+
}
|
|
392
|
+
catch (error) {
|
|
393
|
+
this.logger.error('Failed to get suggestions', { query, error: error.message });
|
|
394
|
+
throw this.handleError(error, 'getSuggestions');
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
/**
|
|
398
|
+
* Get query suggestions configuration (store-specific).
|
|
399
|
+
* Uses GET /api/v1/stores/{storeId}/query-suggestions/config via SDKQuerySuggestionsConfigApi.
|
|
400
|
+
*/
|
|
401
|
+
async getSuggestionsConfig() {
|
|
402
|
+
this.logger.verbose('Getting suggestions configuration');
|
|
403
|
+
try {
|
|
404
|
+
const headers = {
|
|
405
|
+
'x-storeid': this.storeId,
|
|
406
|
+
'x-storesecret': this.readSecret,
|
|
407
|
+
};
|
|
408
|
+
try {
|
|
409
|
+
const response = await this.suggestionsConfigApi.apiV1StoresXStoreIDQuerySuggestionsConfigGet(this.storeId, this.readSecret, this.storeId, { headers });
|
|
410
|
+
const data = response.data?.data;
|
|
411
|
+
const config = data?.metadata?.config ?? data?.config ?? data;
|
|
412
|
+
this.logger.info('Suggestions configuration retrieved', {
|
|
413
|
+
configKeys: config ? Object.keys(config) : [],
|
|
414
|
+
status: response.status,
|
|
415
|
+
});
|
|
416
|
+
return config;
|
|
417
|
+
}
|
|
418
|
+
catch (e) {
|
|
419
|
+
this.logger.verbose('Store config endpoint failed, trying legacy endpoint', { message: e?.message });
|
|
420
|
+
const response = await this.suggestionsApi.v1SuggestionsConfigGet(this.storeId, this.readSecret, { headers });
|
|
421
|
+
const config = response.data?.data;
|
|
422
|
+
this.logger.info('Suggestions configuration retrieved (legacy)', {
|
|
423
|
+
configKeys: config ? Object.keys(config) : [],
|
|
424
|
+
status: response.status,
|
|
425
|
+
});
|
|
426
|
+
return config;
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
catch (error) {
|
|
430
|
+
this.logger.error('Failed to get suggestions config', { error: error.message });
|
|
431
|
+
throw this.handleError(error, 'getSuggestionsConfig');
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
/**
|
|
435
|
+
* Get store configuration
|
|
436
|
+
* Returns store search configuration and onboarding status
|
|
437
|
+
*/
|
|
438
|
+
async getConfig() {
|
|
439
|
+
this.logger.verbose('Getting store configuration');
|
|
440
|
+
try {
|
|
441
|
+
this.logger.verbose('Fetching store configuration', {
|
|
442
|
+
endpoint: `/api/v1/stores/${this.storeId}/config`
|
|
443
|
+
});
|
|
444
|
+
// Build headers with personalization support
|
|
445
|
+
const headers = {
|
|
446
|
+
'x-storeid': this.storeId,
|
|
447
|
+
'x-storesecret': this.readSecret,
|
|
448
|
+
};
|
|
449
|
+
// Add personalization headers if available
|
|
450
|
+
if (this.userId) {
|
|
451
|
+
headers['x-user-id'] = this.userId;
|
|
452
|
+
}
|
|
453
|
+
if (this.anonId) {
|
|
454
|
+
headers['x-anon-id'] = this.anonId;
|
|
455
|
+
}
|
|
456
|
+
if (this.sessionId) {
|
|
457
|
+
headers['x-session-id'] = this.sessionId;
|
|
458
|
+
}
|
|
459
|
+
const response = await this.storesApi.apiV1StoresXStoreIDConfigGet(this.storeId, this.readSecret, this.storeId, { headers });
|
|
460
|
+
const config = response.data?.data;
|
|
461
|
+
this.logger.info('Store configuration retrieved', {
|
|
462
|
+
status: response.status,
|
|
463
|
+
hasConfig: !!config
|
|
464
|
+
});
|
|
465
|
+
return config;
|
|
466
|
+
}
|
|
467
|
+
catch (error) {
|
|
468
|
+
this.logger.error('Failed to get store config', { error: error.message });
|
|
469
|
+
throw this.handleError(error, 'getConfig');
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
/**
|
|
473
|
+
* Get store information
|
|
474
|
+
* Returns store metadata including name, status, and configuration details
|
|
475
|
+
*
|
|
476
|
+
* Note: This extracts information from the store config response.
|
|
477
|
+
* Store name may be derived from config fields if not directly available.
|
|
478
|
+
*
|
|
479
|
+
* @returns Store information object
|
|
480
|
+
*/
|
|
481
|
+
async getStoreInfo() {
|
|
482
|
+
this.logger.verbose('Getting store information');
|
|
483
|
+
try {
|
|
484
|
+
const configResponse = await this.getConfig();
|
|
485
|
+
// Extract store info from config response
|
|
486
|
+
const storeInfo = {
|
|
487
|
+
storeId: this.storeId,
|
|
488
|
+
};
|
|
489
|
+
// The config response structure: { config: IndexConfig, onboarding_status: string }
|
|
490
|
+
if (configResponse) {
|
|
491
|
+
// Handle different response structures
|
|
492
|
+
const config = configResponse.config || configResponse;
|
|
493
|
+
const onboardingStatus = configResponse.onboarding_status;
|
|
494
|
+
if (onboardingStatus) {
|
|
495
|
+
storeInfo.onboardingStatus = onboardingStatus;
|
|
496
|
+
}
|
|
497
|
+
if (config) {
|
|
498
|
+
// Extract store name from various possible fields
|
|
499
|
+
storeInfo.storeName =
|
|
500
|
+
config.store_name ||
|
|
501
|
+
config.storeName ||
|
|
502
|
+
config.name ||
|
|
503
|
+
config.title ||
|
|
504
|
+
config.collection_name ||
|
|
505
|
+
`Store ${this.storeId.substring(0, 8).toUpperCase()}`;
|
|
506
|
+
// Extract query and facet fields
|
|
507
|
+
if (config.query_by) {
|
|
508
|
+
storeInfo.queryFields = Array.isArray(config.query_by)
|
|
509
|
+
? config.query_by
|
|
510
|
+
: [config.query_by];
|
|
511
|
+
}
|
|
512
|
+
if (config.facet_by) {
|
|
513
|
+
storeInfo.facetFields = Array.isArray(config.facet_by)
|
|
514
|
+
? config.facet_by
|
|
515
|
+
: [config.facet_by];
|
|
516
|
+
}
|
|
517
|
+
if (config.primary_text) {
|
|
518
|
+
storeInfo.primaryField = config.primary_text;
|
|
519
|
+
}
|
|
520
|
+
// Store the full config for reference
|
|
521
|
+
storeInfo.config = config;
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
this.logger.info('Store information retrieved', {
|
|
525
|
+
storeId: storeInfo.storeId,
|
|
526
|
+
storeName: storeInfo.storeName
|
|
527
|
+
});
|
|
528
|
+
return storeInfo;
|
|
529
|
+
}
|
|
530
|
+
catch (error) {
|
|
531
|
+
this.logger.error('Failed to get store info', { error: error.message });
|
|
532
|
+
throw this.handleError(error, 'getStoreInfo');
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
/**
|
|
536
|
+
* Update store configuration (requires write secret)
|
|
537
|
+
*/
|
|
538
|
+
async updateConfig(config) {
|
|
539
|
+
this.logger.verbose('Updating store configuration', { configKeys: Object.keys(config) });
|
|
540
|
+
if (!this.writeSecret) {
|
|
541
|
+
this.logger.error('Write secret required but not provided', {
|
|
542
|
+
operation: 'updateConfig',
|
|
543
|
+
storeId: this.storeId
|
|
544
|
+
});
|
|
545
|
+
throw new Error('Write secret is required for updateConfig');
|
|
546
|
+
}
|
|
547
|
+
try {
|
|
548
|
+
this.logger.verbose('Updating store configuration', {
|
|
549
|
+
endpoint: `/api/v1/stores/${this.storeId}/config`,
|
|
550
|
+
configKeys: Object.keys(config)
|
|
551
|
+
});
|
|
552
|
+
// Build headers with write secret
|
|
553
|
+
const headers = {
|
|
554
|
+
'x-storeid': this.storeId,
|
|
555
|
+
'x-storesecret': this.readSecret,
|
|
556
|
+
'x-store-write-secret': this.writeSecret,
|
|
557
|
+
};
|
|
558
|
+
const response = await this.storesApi.apiV1StoresXStoreIDConfigPut(this.storeId, this.writeSecret, this.storeId, config, { headers });
|
|
559
|
+
const updatedConfig = response.data?.data;
|
|
560
|
+
this.logger.info('Store configuration updated successfully', {
|
|
561
|
+
status: response.status,
|
|
562
|
+
hasConfig: !!updatedConfig
|
|
563
|
+
});
|
|
564
|
+
return updatedConfig;
|
|
565
|
+
}
|
|
566
|
+
catch (error) {
|
|
567
|
+
this.logger.error('Failed to update store config', { error: error.message });
|
|
568
|
+
throw this.handleError(error, 'updateConfig');
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
/**
|
|
572
|
+
* Update query suggestions configuration (requires write secret).
|
|
573
|
+
* Uses PUT /api/v1/stores/{xStoreID}/query-suggestions/config via SDKQuerySuggestionsConfigApi.
|
|
574
|
+
*/
|
|
575
|
+
async updateQuerySuggestionsConfig(config) {
|
|
576
|
+
this.logger.verbose('Updating query suggestions configuration', { configKeys: Object.keys(config) });
|
|
577
|
+
if (!this.writeSecret) {
|
|
578
|
+
this.logger.error('Write secret required but not provided', {
|
|
579
|
+
operation: 'updateQuerySuggestionsConfig',
|
|
580
|
+
storeId: this.storeId,
|
|
581
|
+
});
|
|
582
|
+
throw new Error('Write secret is required for updateQuerySuggestionsConfig');
|
|
583
|
+
}
|
|
584
|
+
try {
|
|
585
|
+
const headers = {
|
|
586
|
+
'x-storeid': this.storeId,
|
|
587
|
+
'x-store-write-secret': this.writeSecret,
|
|
588
|
+
'Content-Type': 'application/json',
|
|
589
|
+
};
|
|
590
|
+
try {
|
|
591
|
+
const response = await this.suggestionsConfigApi.apiV1StoresXStoreIDQuerySuggestionsConfigPut(this.storeId, this.writeSecret, this.storeId, config, { headers });
|
|
592
|
+
const data = response.data?.data;
|
|
593
|
+
const updatedConfig = data?.metadata?.config ?? data?.config ?? data;
|
|
594
|
+
this.logger.info('Query suggestions configuration updated successfully', {
|
|
595
|
+
status: response.status,
|
|
596
|
+
hasConfig: !!updatedConfig,
|
|
597
|
+
});
|
|
598
|
+
return updatedConfig;
|
|
599
|
+
}
|
|
600
|
+
catch (apiError) {
|
|
601
|
+
this.logger.verbose('PUT via generated API failed, using direct HTTP', { message: apiError?.message });
|
|
602
|
+
const axiosInstance = this.suggestionsConfigApi.axios ?? this.suggestionsApi.axios ?? axios_1.default;
|
|
603
|
+
const baseUrl = this.config.basePath ?? 'https://api.seekora.com/api';
|
|
604
|
+
const response = await axiosInstance.put(`${baseUrl}/v1/stores/${this.storeId}/query-suggestions/config`, config, { headers });
|
|
605
|
+
const data = response.data?.data;
|
|
606
|
+
const updatedConfig = data?.metadata?.config ?? data?.config ?? data;
|
|
607
|
+
this.logger.info('Query suggestions configuration updated successfully', {
|
|
608
|
+
status: response.status,
|
|
609
|
+
hasConfig: !!updatedConfig,
|
|
610
|
+
});
|
|
611
|
+
return updatedConfig;
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
catch (error) {
|
|
615
|
+
this.logger.error('Failed to update query suggestions config', { error: error.message });
|
|
616
|
+
throw this.handleError(error, 'updateQuerySuggestionsConfig');
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
/**
|
|
620
|
+
* Get configuration schema
|
|
621
|
+
*/
|
|
622
|
+
async getConfigSchema() {
|
|
623
|
+
this.logger.verbose('Getting configuration schema');
|
|
624
|
+
try {
|
|
625
|
+
const response = await this.storesApi.apiV1StoresXStoreIDConfigSchemaGet(this.storeId, this.readSecret, this.storeId, {
|
|
626
|
+
headers: {
|
|
627
|
+
'x-storeid': this.storeId,
|
|
628
|
+
'x-storesecret': this.readSecret,
|
|
629
|
+
}
|
|
630
|
+
});
|
|
631
|
+
const schema = response.data?.data;
|
|
632
|
+
this.logger.verbose('Configuration schema retrieved', { schemaKeys: schema ? Object.keys(schema) : [] });
|
|
633
|
+
return schema;
|
|
634
|
+
}
|
|
635
|
+
catch (error) {
|
|
636
|
+
this.logger.error('Failed to get config schema', { error: error.message });
|
|
637
|
+
throw this.handleError(error, 'getConfigSchema');
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
/**
|
|
641
|
+
* Build event payload with identifiers
|
|
642
|
+
* Ensures user_id or anon_id is present, and sets correlation_id/search_id at top level
|
|
643
|
+
*/
|
|
644
|
+
buildEventPayload(event, context) {
|
|
645
|
+
const payload = {
|
|
646
|
+
...event,
|
|
647
|
+
// Set identifiers from context or fall back to client defaults
|
|
648
|
+
user_id: event.user_id || context?.userId || this.userId,
|
|
649
|
+
anon_id: event.anon_id || context?.anonId || this.anonId,
|
|
650
|
+
session_id: event.session_id || context?.sessionId || this.sessionId,
|
|
651
|
+
// Set correlation_id at top level if provided (per identifier spec)
|
|
652
|
+
correlation_id: event.correlation_id || context?.correlationId,
|
|
653
|
+
// Set search_id at top level (not just in metadata) if provided (per identifier spec)
|
|
654
|
+
search_id: event.search_id || context?.searchId,
|
|
655
|
+
};
|
|
656
|
+
// Ensure either user_id or anon_id is present
|
|
657
|
+
if (!payload.user_id && !payload.anon_id) {
|
|
658
|
+
// This should not happen as we always have anonId, but add check for safety
|
|
659
|
+
payload.anon_id = this.anonId;
|
|
660
|
+
}
|
|
661
|
+
// Also include search_id in metadata for backward compatibility
|
|
662
|
+
if (payload.search_id && !payload.metadata) {
|
|
663
|
+
payload.metadata = {};
|
|
664
|
+
}
|
|
665
|
+
if (payload.search_id && payload.metadata && !payload.metadata.search_id) {
|
|
666
|
+
payload.metadata.search_id = payload.search_id;
|
|
667
|
+
}
|
|
668
|
+
return payload;
|
|
669
|
+
}
|
|
670
|
+
/**
|
|
671
|
+
* Track a single analytics event
|
|
672
|
+
*
|
|
673
|
+
* @param event - Event payload (partial, will be enriched with identifiers)
|
|
674
|
+
* @param context - Optional search context for linking events to searches
|
|
675
|
+
*/
|
|
676
|
+
async trackEvent(event, context) {
|
|
677
|
+
this.logger.verbose('Tracking analytics event', {
|
|
678
|
+
eventName: event.event_name,
|
|
679
|
+
hasContext: !!context
|
|
680
|
+
});
|
|
681
|
+
const payload = this.buildEventPayload(event, context);
|
|
682
|
+
try {
|
|
683
|
+
this.logger.verbose('Sending analytics event', {
|
|
684
|
+
endpoint: '/api/analytics/event',
|
|
685
|
+
eventName: event.event_name
|
|
686
|
+
});
|
|
687
|
+
// Cast to DataTypesEventPayload for API call (backend accepts extended fields)
|
|
688
|
+
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
|
|
689
|
+
{
|
|
690
|
+
headers: {
|
|
691
|
+
'x-storeid': this.storeId,
|
|
692
|
+
'x-storesecret': this.readSecret,
|
|
693
|
+
}
|
|
694
|
+
});
|
|
695
|
+
this.logger.verbose('Analytics event tracked successfully', {
|
|
696
|
+
eventName: event.event_name,
|
|
697
|
+
status: response.status
|
|
698
|
+
});
|
|
699
|
+
}
|
|
700
|
+
catch (error) {
|
|
701
|
+
this.logger.error('Failed to track event', { eventName: event.event_name, error: error.message });
|
|
702
|
+
throw this.handleError(error, 'trackEvent');
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
/**
|
|
706
|
+
* Track multiple analytics events (batch)
|
|
707
|
+
*
|
|
708
|
+
* Note: Events should already have identifiers set. Consider using individual trackEvent()
|
|
709
|
+
* calls or buildEventPayload() to ensure identifiers are properly set.
|
|
710
|
+
*
|
|
711
|
+
* @param events - Array of event payloads (should have identifiers already set)
|
|
712
|
+
*/
|
|
713
|
+
async trackEvents(events, context) {
|
|
714
|
+
this.logger.verbose('Tracking batch analytics events', {
|
|
715
|
+
count: events.length,
|
|
716
|
+
hasContext: !!context
|
|
717
|
+
});
|
|
718
|
+
if (events.length > 100) {
|
|
719
|
+
this.logger.error('Batch size exceeds maximum', { count: events.length, max: 100 });
|
|
720
|
+
throw new Error('Maximum 100 events per batch');
|
|
721
|
+
}
|
|
722
|
+
try {
|
|
723
|
+
this.logger.verbose('Sending batch analytics events', {
|
|
724
|
+
endpoint: '/api/analytics/batch',
|
|
725
|
+
eventCount: events.length
|
|
726
|
+
});
|
|
727
|
+
// Build full event payloads with identifiers
|
|
728
|
+
const enrichedEvents = events.map(event => this.buildEventPayload(event, context));
|
|
729
|
+
const batchRequest = {
|
|
730
|
+
events: enrichedEvents // Type assertion for extended payloads
|
|
731
|
+
};
|
|
732
|
+
const response = await this.analyticsApi.analyticsBatchPost(this.storeId, this.readSecret, batchRequest, {
|
|
733
|
+
headers: {
|
|
734
|
+
'x-storeid': this.storeId,
|
|
735
|
+
'x-storesecret': this.readSecret,
|
|
736
|
+
}
|
|
737
|
+
});
|
|
738
|
+
this.logger.info('Batch analytics events tracked successfully', {
|
|
739
|
+
count: events.length,
|
|
740
|
+
status: response.status
|
|
741
|
+
});
|
|
742
|
+
}
|
|
743
|
+
catch (error) {
|
|
744
|
+
this.logger.error('Failed to track batch events', { count: events.length, error: error.message });
|
|
745
|
+
throw this.handleError(error, 'trackEvents');
|
|
746
|
+
}
|
|
747
|
+
}
|
|
748
|
+
/**
|
|
749
|
+
* Track a search event
|
|
750
|
+
*
|
|
751
|
+
* @param params - Search tracking parameters
|
|
752
|
+
*/
|
|
753
|
+
async trackSearch(params) {
|
|
754
|
+
this.logger.verbose('Tracking search event', {
|
|
755
|
+
query: params.query,
|
|
756
|
+
resultsCount: params.resultsCount,
|
|
757
|
+
hasContext: !!params.context
|
|
758
|
+
});
|
|
759
|
+
await this.trackEvent({
|
|
760
|
+
event_name: 'search.performed',
|
|
761
|
+
query: params.query,
|
|
762
|
+
results_count: params.resultsCount,
|
|
763
|
+
analytics_tags: params.analyticsTags,
|
|
764
|
+
metadata: params.metadata,
|
|
765
|
+
}, params.context);
|
|
766
|
+
}
|
|
767
|
+
/**
|
|
768
|
+
* Track an impression event
|
|
769
|
+
*
|
|
770
|
+
* @param params - Impression tracking parameters
|
|
771
|
+
*/
|
|
772
|
+
async trackImpression(params) {
|
|
773
|
+
this.logger.verbose('Tracking impression event', {
|
|
774
|
+
itemId: params.itemId,
|
|
775
|
+
position: params.position,
|
|
776
|
+
hasContext: !!params.context
|
|
777
|
+
});
|
|
778
|
+
await this.trackEvent({
|
|
779
|
+
event_name: 'impression',
|
|
780
|
+
clicked_item_id: params.itemId,
|
|
781
|
+
metadata: {
|
|
782
|
+
position: params.position,
|
|
783
|
+
list_type: params.listType,
|
|
784
|
+
search_query: params.searchQuery,
|
|
785
|
+
...params.metadata,
|
|
786
|
+
},
|
|
787
|
+
}, params.context);
|
|
788
|
+
}
|
|
789
|
+
async trackClick(itemId, position, contextOrSearchId) {
|
|
790
|
+
this.logger.verbose('Tracking click event', {
|
|
791
|
+
itemId,
|
|
792
|
+
position,
|
|
793
|
+
hasContext: !!contextOrSearchId
|
|
794
|
+
});
|
|
795
|
+
// Handle backward compatibility: if third param is string, treat as searchId
|
|
796
|
+
const context = typeof contextOrSearchId === 'string'
|
|
797
|
+
? { correlationId: '', searchId: contextOrSearchId, anonId: this.anonId, sessionId: this.sessionId }
|
|
798
|
+
: contextOrSearchId;
|
|
799
|
+
await this.trackEvent({
|
|
800
|
+
event_name: 'product_click',
|
|
801
|
+
clicked_item_id: itemId,
|
|
802
|
+
metadata: {
|
|
803
|
+
position,
|
|
804
|
+
},
|
|
805
|
+
}, context);
|
|
806
|
+
}
|
|
807
|
+
async trackView(itemId, contextOrSearchId) {
|
|
808
|
+
this.logger.verbose('Tracking view event', {
|
|
809
|
+
itemId,
|
|
810
|
+
hasContext: !!contextOrSearchId
|
|
811
|
+
});
|
|
812
|
+
// Handle backward compatibility: if second param is string, treat as searchId
|
|
813
|
+
const context = typeof contextOrSearchId === 'string'
|
|
814
|
+
? { correlationId: '', searchId: contextOrSearchId, anonId: this.anonId, sessionId: this.sessionId }
|
|
815
|
+
: contextOrSearchId;
|
|
816
|
+
await this.trackEvent({
|
|
817
|
+
event_name: 'view',
|
|
818
|
+
clicked_item_id: itemId,
|
|
819
|
+
}, context);
|
|
820
|
+
}
|
|
821
|
+
async trackConversion(itemId, value, currency, contextOrSearchId) {
|
|
822
|
+
this.logger.verbose('Tracking conversion event', {
|
|
823
|
+
itemId,
|
|
824
|
+
value,
|
|
825
|
+
currency,
|
|
826
|
+
hasContext: !!contextOrSearchId
|
|
827
|
+
});
|
|
828
|
+
// Handle backward compatibility: if fourth param is string, treat as searchId
|
|
829
|
+
const context = typeof contextOrSearchId === 'string'
|
|
830
|
+
? { correlationId: '', searchId: contextOrSearchId, anonId: this.anonId, sessionId: this.sessionId }
|
|
831
|
+
: contextOrSearchId;
|
|
832
|
+
await this.trackEvent({
|
|
833
|
+
event_name: 'conversion',
|
|
834
|
+
clicked_item_id: itemId,
|
|
835
|
+
value,
|
|
836
|
+
currency,
|
|
837
|
+
}, context);
|
|
838
|
+
}
|
|
839
|
+
/**
|
|
840
|
+
* Track a custom event
|
|
841
|
+
*
|
|
842
|
+
* @param eventName - Custom event name
|
|
843
|
+
* @param payload - Additional event data
|
|
844
|
+
* @param context - Optional search context for linking events to searches
|
|
845
|
+
*/
|
|
846
|
+
async trackCustom(eventName, payload = {}, context) {
|
|
847
|
+
this.logger.verbose('Tracking custom event', {
|
|
848
|
+
eventName,
|
|
849
|
+
hasContext: !!context
|
|
850
|
+
});
|
|
851
|
+
await this.trackEvent({
|
|
852
|
+
...payload,
|
|
853
|
+
event_name: eventName,
|
|
854
|
+
}, context);
|
|
855
|
+
}
|
|
856
|
+
/**
|
|
857
|
+
* Validate an event payload before sending
|
|
858
|
+
* Useful for debugging and ensuring events are properly formatted
|
|
859
|
+
*
|
|
860
|
+
* @param event - Event payload to validate
|
|
861
|
+
* @returns Validation result with success status and any error messages
|
|
862
|
+
*/
|
|
863
|
+
async validateEvent(event) {
|
|
864
|
+
this.logger.verbose('Validating event payload', { eventName: event.event_name });
|
|
865
|
+
try {
|
|
866
|
+
this.logger.verbose('Validating event payload', {
|
|
867
|
+
endpoint: '/api/analytics/validate',
|
|
868
|
+
eventName: event.event_name
|
|
869
|
+
});
|
|
870
|
+
const payload = this.buildEventPayload(event);
|
|
871
|
+
const response = await this.analyticsApi.analyticsValidatePost(this.storeId, this.readSecret, payload, {
|
|
872
|
+
headers: {
|
|
873
|
+
'x-storeid': this.storeId,
|
|
874
|
+
'x-storesecret': this.readSecret,
|
|
875
|
+
}
|
|
876
|
+
});
|
|
877
|
+
const result = response.data?.data || response.data;
|
|
878
|
+
const isValid = result?.valid !== false && response.status === 200;
|
|
879
|
+
this.logger.verbose('Event validation result', {
|
|
880
|
+
valid: isValid,
|
|
881
|
+
eventName: event.event_name,
|
|
882
|
+
message: result?.message
|
|
883
|
+
});
|
|
884
|
+
return {
|
|
885
|
+
valid: isValid,
|
|
886
|
+
message: result?.message || (isValid ? 'Event is valid' : 'Event validation failed'),
|
|
887
|
+
errors: result?.errors,
|
|
888
|
+
};
|
|
889
|
+
}
|
|
890
|
+
catch (error) {
|
|
891
|
+
this.logger.error('Event validation failed', { error: error.message });
|
|
892
|
+
return {
|
|
893
|
+
valid: false,
|
|
894
|
+
message: error.response?.data?.message || error.message || 'Validation request failed',
|
|
895
|
+
errors: error.response?.data?.errors || error.response?.data,
|
|
896
|
+
};
|
|
897
|
+
}
|
|
898
|
+
}
|
|
899
|
+
/**
|
|
900
|
+
* Get event schema
|
|
901
|
+
* Returns the JSON schema for event payloads, useful for understanding required/optional fields
|
|
902
|
+
*
|
|
903
|
+
* @returns Event schema with field definitions, types, and validation rules
|
|
904
|
+
*/
|
|
905
|
+
async getEventSchema() {
|
|
906
|
+
this.logger.verbose('Getting event schema');
|
|
907
|
+
try {
|
|
908
|
+
const response = await this.analyticsApi.analyticsSchemaGet(this.storeId, this.readSecret, {
|
|
909
|
+
headers: {
|
|
910
|
+
'x-storeid': this.storeId,
|
|
911
|
+
'x-storesecret': this.readSecret,
|
|
912
|
+
}
|
|
913
|
+
});
|
|
914
|
+
const schema = response.data?.data || response.data;
|
|
915
|
+
this.logger.verbose('Event schema retrieved', { schemaKeys: schema ? Object.keys(schema) : [] });
|
|
916
|
+
return schema;
|
|
917
|
+
}
|
|
918
|
+
catch (error) {
|
|
919
|
+
this.logger.error('Failed to get event schema', { error: error.message });
|
|
920
|
+
throw this.handleError(error, 'getEventSchema');
|
|
921
|
+
}
|
|
922
|
+
}
|
|
923
|
+
/**
|
|
924
|
+
* Set user ID (call this when user logs in)
|
|
925
|
+
*
|
|
926
|
+
* @param userId - Authenticated user ID
|
|
927
|
+
*/
|
|
928
|
+
setUserId(userId) {
|
|
929
|
+
this.userId = userId;
|
|
930
|
+
this.logger.info('User ID updated', { userId });
|
|
931
|
+
}
|
|
932
|
+
/**
|
|
933
|
+
* Clear user ID (call this when user logs out)
|
|
934
|
+
*/
|
|
935
|
+
clearUserId() {
|
|
936
|
+
this.userId = undefined;
|
|
937
|
+
this.logger.info('User ID cleared');
|
|
938
|
+
}
|
|
939
|
+
/**
|
|
940
|
+
* Get current identifiers
|
|
941
|
+
* Useful for debugging or custom event tracking
|
|
942
|
+
*/
|
|
943
|
+
getIdentifiers() {
|
|
944
|
+
return {
|
|
945
|
+
userId: this.userId,
|
|
946
|
+
anonId: this.anonId,
|
|
947
|
+
sessionId: this.sessionId,
|
|
948
|
+
};
|
|
949
|
+
}
|
|
950
|
+
/**
|
|
951
|
+
* Handle errors consistently
|
|
952
|
+
*/
|
|
953
|
+
handleError(error, operation) {
|
|
954
|
+
if (error.response) {
|
|
955
|
+
const status = error.response.status;
|
|
956
|
+
const data = error.response.data;
|
|
957
|
+
const message = data?.message || `API request failed with status ${status}`;
|
|
958
|
+
// Log based on status code
|
|
959
|
+
if (status >= 500) {
|
|
960
|
+
this.logger.error('API server error', { operation, status, message });
|
|
961
|
+
}
|
|
962
|
+
else if (status === 401 || status === 403) {
|
|
963
|
+
this.logger.error('Authentication error', { operation, status, message });
|
|
964
|
+
}
|
|
965
|
+
else if (status === 429) {
|
|
966
|
+
this.logger.warn('Rate limit exceeded', { operation, status, message });
|
|
967
|
+
}
|
|
968
|
+
else {
|
|
969
|
+
this.logger.error('API request failed', { operation, status, message });
|
|
970
|
+
}
|
|
971
|
+
return new Error(`[${operation}] ${message} (${status})`);
|
|
972
|
+
}
|
|
973
|
+
else if (error.request) {
|
|
974
|
+
this.logger.error('Network error', { operation, error: error.message });
|
|
975
|
+
return new Error(`[${operation}] Network error: ${error.message}`);
|
|
976
|
+
}
|
|
977
|
+
else {
|
|
978
|
+
this.logger.error('Request error', { operation, error: error.message });
|
|
979
|
+
return new Error(`[${operation}] ${error.message}`);
|
|
980
|
+
}
|
|
981
|
+
}
|
|
982
|
+
/**
|
|
983
|
+
* Get the logger instance
|
|
984
|
+
*/
|
|
985
|
+
getLogger() {
|
|
986
|
+
return this.logger;
|
|
987
|
+
}
|
|
988
|
+
}
|
|
989
|
+
exports.SeekoraClient = SeekoraClient;
|
|
990
|
+
// Export default
|
|
991
|
+
exports.default = SeekoraClient;
|