@seekora-ai/search-sdk 0.2.21 → 0.2.23
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/analytics/activeFilters.d.ts +33 -0
- package/dist/analytics/activeFilters.js +145 -0
- package/dist/analytics/events.d.ts +26 -0
- package/dist/analytics/events.js +38 -0
- package/dist/analytics/index.d.ts +6 -0
- package/dist/analytics/index.js +55 -0
- package/dist/analytics/loadAnalytics.d.ts +37 -0
- package/dist/analytics/loadAnalytics.js +70 -0
- package/dist/analytics/searchId.d.ts +26 -0
- package/dist/analytics/searchId.js +102 -0
- package/dist/analytics/track.d.ts +59 -0
- package/dist/analytics/track.js +130 -0
- package/dist/analytics/types.gen.d.ts +781 -0
- package/dist/analytics/types.gen.js +6 -0
- package/dist/client.d.ts +82 -17
- package/dist/client.js +588 -414
- package/dist/index.d.ts +13 -12
- package/dist/index.js +39 -1
- package/package.json +9 -3
package/dist/client.js
CHANGED
|
@@ -9,6 +9,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
9
9
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
10
10
|
exports.SeekoraClient = void 0;
|
|
11
11
|
const generated_1 = require("./generated");
|
|
12
|
+
const analytics_next_1 = require("@segment/analytics-next");
|
|
12
13
|
const axios_1 = __importDefault(require("axios"));
|
|
13
14
|
const config_1 = require("./config");
|
|
14
15
|
const logger_1 = require("./logger");
|
|
@@ -16,15 +17,34 @@ const config_loader_1 = require("./config-loader");
|
|
|
16
17
|
const utils_1 = require("./utils");
|
|
17
18
|
const context_collector_1 = require("./context-collector");
|
|
18
19
|
const event_queue_1 = require("./event-queue");
|
|
20
|
+
const searchId_1 = require("./analytics/searchId");
|
|
19
21
|
/**
|
|
20
22
|
* Seekora SDK Client
|
|
21
23
|
*
|
|
22
24
|
* Provides a clean, easy-to-use interface for the Seekora Search API
|
|
23
25
|
*/
|
|
24
26
|
class SeekoraClient {
|
|
27
|
+
/**
|
|
28
|
+
* Lazy-init analytics-next. Returns null when Jitsu config is incomplete
|
|
29
|
+
* (jitsuWriteKey missing) so callers no-op rather than firing events
|
|
30
|
+
* with the wrong credential to the wrong endpoint. SCRUM-259.
|
|
31
|
+
*/
|
|
32
|
+
getAnalytics() {
|
|
33
|
+
const writeKey = this.clientConfig.jitsuWriteKey;
|
|
34
|
+
if (!writeKey)
|
|
35
|
+
return null;
|
|
36
|
+
if (!this._analytics) {
|
|
37
|
+
this._analytics = analytics_next_1.AnalyticsBrowser.load({
|
|
38
|
+
writeKey,
|
|
39
|
+
cdnURL: this.clientConfig.jitsuIngestUrl,
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
return this._analytics;
|
|
43
|
+
}
|
|
25
44
|
constructor(config = {}) {
|
|
26
45
|
this.cachedBrowserContext = null;
|
|
27
46
|
this.eventQueue = null;
|
|
47
|
+
this._analytics = null;
|
|
28
48
|
this.clientConfig = config;
|
|
29
49
|
// Load configuration from file, env, and code (in that order)
|
|
30
50
|
const mergedConfig = (0, config_loader_1.loadConfig)(config);
|
|
@@ -62,34 +82,34 @@ class SeekoraClient {
|
|
|
62
82
|
this.enableDeduplication = config.enableDeduplication || false;
|
|
63
83
|
this.deduplicationWindow = config.deduplicationWindow || 300000; // Default: 5 minutes
|
|
64
84
|
// Log identifier initialization
|
|
65
|
-
this.logger.verbose(
|
|
85
|
+
this.logger.verbose("Client identifiers initialized", {
|
|
66
86
|
hasUserId: !!this.userId,
|
|
67
|
-
anonId: this.anonId.substring(0, 8) +
|
|
68
|
-
sessionId: this.sessionId.substring(0, 8) +
|
|
87
|
+
anonId: this.anonId.substring(0, 8) + "...", // Log partial ID for privacy
|
|
88
|
+
sessionId: this.sessionId.substring(0, 8) + "...",
|
|
69
89
|
autoTrackSearch: this.autoTrackSearch,
|
|
70
90
|
enableContextCollection: this.enableContextCollection,
|
|
71
91
|
enableEventQueue: this.enableEventQueue,
|
|
72
92
|
enableDeduplication: this.enableDeduplication,
|
|
73
|
-
deduplicationWindow: this.deduplicationWindow
|
|
93
|
+
deduplicationWindow: this.deduplicationWindow,
|
|
74
94
|
});
|
|
75
|
-
this.logger.verbose(
|
|
95
|
+
this.logger.verbose("Initializing SeekoraClient", {
|
|
76
96
|
storeId: this.storeId,
|
|
77
97
|
environment: mergedConfig.environment,
|
|
78
98
|
logLevel,
|
|
79
99
|
});
|
|
80
100
|
// Get base URL from environment or config
|
|
81
101
|
const baseUrl = (0, config_1.getBaseUrl)(mergedConfig.environment, mergedConfig.baseUrl);
|
|
82
|
-
this.logger.verbose(
|
|
102
|
+
this.logger.verbose("Using base URL", { baseUrl });
|
|
83
103
|
// Create configuration with base URL
|
|
84
104
|
this.config = new generated_1.Configuration({
|
|
85
105
|
basePath: baseUrl,
|
|
86
106
|
baseOptions: {
|
|
87
107
|
timeout: mergedConfig.timeout || 30000,
|
|
88
108
|
headers: {
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
}
|
|
92
|
-
}
|
|
109
|
+
"x-storeid": this.storeId,
|
|
110
|
+
"x-storesecret": this.readSecret,
|
|
111
|
+
},
|
|
112
|
+
},
|
|
93
113
|
});
|
|
94
114
|
// Initialize API clients
|
|
95
115
|
this.searchApi = new generated_1.SearchApi(this.config);
|
|
@@ -97,7 +117,7 @@ class SeekoraClient {
|
|
|
97
117
|
this.analyticsApi = new generated_1.AnalyticsEventsApi(this.config);
|
|
98
118
|
this.storeManagementApi = new generated_1.StoreManagementApi(this.config);
|
|
99
119
|
this.filtersApi = new generated_1.FiltersApi(this.config);
|
|
100
|
-
this.logger.info(
|
|
120
|
+
this.logger.info("SeekoraClient initialized successfully");
|
|
101
121
|
}
|
|
102
122
|
/**
|
|
103
123
|
* Search for documents
|
|
@@ -111,14 +131,14 @@ class SeekoraClient {
|
|
|
111
131
|
*/
|
|
112
132
|
async search(query, options) {
|
|
113
133
|
// Convert empty query to wildcard
|
|
114
|
-
const searchQuery = query.trim() ||
|
|
134
|
+
const searchQuery = query.trim() || "*";
|
|
115
135
|
// Generate correlation_id for this search journey
|
|
116
136
|
const correlationId = (0, utils_1.generateUUID)();
|
|
117
|
-
this.logger.verbose(
|
|
137
|
+
this.logger.verbose("Executing search", {
|
|
118
138
|
originalQuery: query,
|
|
119
139
|
searchQuery,
|
|
120
140
|
correlationId,
|
|
121
|
-
options
|
|
141
|
+
options,
|
|
122
142
|
});
|
|
123
143
|
const searchRequest = {
|
|
124
144
|
q: searchQuery,
|
|
@@ -162,32 +182,32 @@ class SeekoraClient {
|
|
|
162
182
|
synonym_sets: options?.synonym_sets,
|
|
163
183
|
};
|
|
164
184
|
// Log search request details (verbose level for debugging)
|
|
165
|
-
this.logger.verbose(
|
|
185
|
+
this.logger.verbose("Search request prepared", {
|
|
166
186
|
filter: searchRequest.filter,
|
|
167
187
|
filterType: typeof searchRequest.filter,
|
|
168
|
-
fullRequest: searchRequest
|
|
188
|
+
fullRequest: searchRequest,
|
|
169
189
|
});
|
|
170
190
|
try {
|
|
171
191
|
// Log API request start
|
|
172
|
-
this.logger.verbose(
|
|
173
|
-
endpoint:
|
|
174
|
-
method:
|
|
175
|
-
storeId: this.storeId
|
|
192
|
+
this.logger.verbose("Sending search API request", {
|
|
193
|
+
endpoint: "/api/v1/search",
|
|
194
|
+
method: "POST",
|
|
195
|
+
storeId: this.storeId,
|
|
176
196
|
});
|
|
177
197
|
// Build headers with personalization support
|
|
178
198
|
const headers = {
|
|
179
|
-
|
|
180
|
-
|
|
199
|
+
"x-storeid": this.storeId,
|
|
200
|
+
"x-storesecret": this.readSecret,
|
|
181
201
|
};
|
|
182
202
|
// Add personalization headers if available
|
|
183
203
|
if (this.userId) {
|
|
184
|
-
headers[
|
|
204
|
+
headers["x-user-id"] = this.userId;
|
|
185
205
|
}
|
|
186
206
|
if (this.anonId) {
|
|
187
|
-
headers[
|
|
207
|
+
headers["x-anon-id"] = this.anonId;
|
|
188
208
|
}
|
|
189
209
|
if (this.sessionId) {
|
|
190
|
-
headers[
|
|
210
|
+
headers["x-session-id"] = this.sessionId;
|
|
191
211
|
}
|
|
192
212
|
// Use POST to avoid URL length limits with complex filters/facets
|
|
193
213
|
const response = await this.searchApi.v1SearchPost(this.storeId, this.readSecret, {
|
|
@@ -201,9 +221,15 @@ class SeekoraClient {
|
|
|
201
221
|
widget_mode: searchRequest.widget_mode,
|
|
202
222
|
include_suggestions: searchRequest.include_suggestions,
|
|
203
223
|
suggestions_limit: searchRequest.suggestions_limit,
|
|
204
|
-
analytics_tags: Array.isArray(searchRequest.analytics_tags)
|
|
205
|
-
|
|
206
|
-
|
|
224
|
+
analytics_tags: Array.isArray(searchRequest.analytics_tags)
|
|
225
|
+
? searchRequest.analytics_tags
|
|
226
|
+
: undefined,
|
|
227
|
+
stopword_sets: Array.isArray(searchRequest.stopword_sets)
|
|
228
|
+
? searchRequest.stopword_sets
|
|
229
|
+
: undefined,
|
|
230
|
+
synonym_sets: Array.isArray(searchRequest.synonym_sets)
|
|
231
|
+
? searchRequest.synonym_sets
|
|
232
|
+
: undefined,
|
|
207
233
|
search_fields: searchRequest.search_fields,
|
|
208
234
|
return_fields: searchRequest.return_fields,
|
|
209
235
|
omit_fields: searchRequest.omit_fields,
|
|
@@ -231,40 +257,58 @@ class SeekoraClient {
|
|
|
231
257
|
facet_search_text: searchRequest.facet_search_text,
|
|
232
258
|
}, this.userId, this.anonId, this.sessionId, { headers });
|
|
233
259
|
// Log API response received
|
|
234
|
-
this.logger.verbose(
|
|
260
|
+
this.logger.verbose("Search API response received", {
|
|
235
261
|
status: response.status,
|
|
236
|
-
hasData: !!response.data
|
|
262
|
+
hasData: !!response.data,
|
|
237
263
|
});
|
|
238
264
|
// Extract search results
|
|
239
265
|
const data = response.data;
|
|
240
266
|
const facets = data?.data?.facets || [];
|
|
241
267
|
// Log API response (verbose level for detailed debugging)
|
|
242
|
-
this.logger.verbose(
|
|
268
|
+
this.logger.verbose("API response received", {
|
|
243
269
|
status: response.status,
|
|
244
270
|
totalResults: data?.data?.total_results || 0,
|
|
245
271
|
resultsCount: data?.data?.results?.length || 0,
|
|
246
|
-
facetsType: Array.isArray(facets) ?
|
|
247
|
-
facetsCount: Array.isArray(facets)
|
|
272
|
+
facetsType: Array.isArray(facets) ? "array" : typeof facets,
|
|
273
|
+
facetsCount: Array.isArray(facets)
|
|
274
|
+
? facets.length
|
|
275
|
+
: Object.keys(facets || {}).length,
|
|
248
276
|
});
|
|
249
277
|
// Convert facets array to a more accessible object format
|
|
250
278
|
const facetsMap = {};
|
|
251
279
|
if (Array.isArray(facets)) {
|
|
252
|
-
this.logger.verbose(
|
|
280
|
+
this.logger.verbose("Processing facets as array", {
|
|
281
|
+
facetsCount: facets.length,
|
|
282
|
+
});
|
|
253
283
|
facets.forEach((facet, index) => {
|
|
254
284
|
// Handle different facet formats
|
|
255
|
-
const fieldName = facet?.field_name ||
|
|
256
|
-
|
|
285
|
+
const fieldName = facet?.field_name ||
|
|
286
|
+
facet?.fieldName ||
|
|
287
|
+
facet?.name ||
|
|
288
|
+
`facet_${index}`;
|
|
289
|
+
if (fieldName &&
|
|
290
|
+
(facet?.counts || facet?.values || Array.isArray(facet))) {
|
|
257
291
|
facetsMap[fieldName] = {
|
|
258
292
|
field_name: fieldName,
|
|
259
|
-
counts: facet.counts ||
|
|
260
|
-
|
|
293
|
+
counts: facet.counts ||
|
|
294
|
+
facet.values ||
|
|
295
|
+
(Array.isArray(facet) ? facet : []),
|
|
296
|
+
stats: facet.stats || {},
|
|
261
297
|
};
|
|
262
|
-
this.logger.verbose(
|
|
298
|
+
this.logger.verbose("Processed facet", {
|
|
299
|
+
fieldName,
|
|
300
|
+
index,
|
|
301
|
+
counts: facetsMap[fieldName].counts?.length || 0,
|
|
302
|
+
});
|
|
263
303
|
}
|
|
264
304
|
});
|
|
265
305
|
}
|
|
266
|
-
else if (facets &&
|
|
267
|
-
|
|
306
|
+
else if (facets &&
|
|
307
|
+
typeof facets === "object" &&
|
|
308
|
+
!Array.isArray(facets)) {
|
|
309
|
+
this.logger.verbose("Processing facets as object", {
|
|
310
|
+
facetKeys: Object.keys(facets),
|
|
311
|
+
});
|
|
268
312
|
// If facets is already an object, use it directly but ensure field_name is set
|
|
269
313
|
Object.keys(facets).forEach((key) => {
|
|
270
314
|
const facetData = facets[key];
|
|
@@ -272,17 +316,29 @@ class SeekoraClient {
|
|
|
272
316
|
facetsMap[key] = {
|
|
273
317
|
field_name: facetData.field_name || key,
|
|
274
318
|
counts: facetData.counts || facetData.values || [],
|
|
275
|
-
stats: facetData.stats || {}
|
|
319
|
+
stats: facetData.stats || {},
|
|
276
320
|
};
|
|
277
321
|
}
|
|
278
322
|
});
|
|
279
323
|
}
|
|
280
|
-
this.logger.verbose(
|
|
324
|
+
this.logger.verbose("Facets processed", {
|
|
281
325
|
facetFields: Object.keys(facetsMap),
|
|
282
|
-
totalFacetFields: Object.keys(facetsMap).length
|
|
326
|
+
totalFacetFields: Object.keys(facetsMap).length,
|
|
283
327
|
});
|
|
284
328
|
// Extract search_id from response (if present)
|
|
285
329
|
const searchId = data?.data?.search_id;
|
|
330
|
+
// V4 emission module: register the freshly-minted search_id so the
|
|
331
|
+
// 3-layer resolver (in-memory → sessionStorage → URL) returns it on
|
|
332
|
+
// subsequent track() calls within the 30-min TTL window. See
|
|
333
|
+
// src/analytics/searchId.ts + spec Section 6C.
|
|
334
|
+
if (searchId) {
|
|
335
|
+
try {
|
|
336
|
+
(0, searchId_1.setActiveSearchId)(searchId);
|
|
337
|
+
}
|
|
338
|
+
catch {
|
|
339
|
+
// Defensive — search must never fail because of analytics wiring.
|
|
340
|
+
}
|
|
341
|
+
}
|
|
286
342
|
// Create search context for linking events
|
|
287
343
|
const context = {
|
|
288
344
|
correlationId,
|
|
@@ -298,9 +354,9 @@ class SeekoraClient {
|
|
|
298
354
|
context, // New: search context for event linking
|
|
299
355
|
facets: facetsMap,
|
|
300
356
|
facet_counts: facetsMap, // Alias for backward compatibility
|
|
301
|
-
...data
|
|
357
|
+
...data,
|
|
302
358
|
};
|
|
303
|
-
this.logger.info(
|
|
359
|
+
this.logger.info("Search completed", {
|
|
304
360
|
query: searchQuery,
|
|
305
361
|
originalQuery: query,
|
|
306
362
|
totalResults: results.totalResults,
|
|
@@ -311,20 +367,25 @@ class SeekoraClient {
|
|
|
311
367
|
if (this.autoTrackSearch) {
|
|
312
368
|
this.trackSearch({
|
|
313
369
|
query: searchQuery,
|
|
314
|
-
|
|
315
|
-
context,
|
|
370
|
+
numFound: results.totalResults,
|
|
316
371
|
}).catch((err) => {
|
|
317
372
|
// Log but don't fail the search if tracking fails
|
|
318
|
-
this.logger.warn(
|
|
373
|
+
this.logger.warn("Failed to auto-track search event", {
|
|
374
|
+
error: err.message,
|
|
375
|
+
});
|
|
319
376
|
});
|
|
320
377
|
}
|
|
321
378
|
// Automatically track search result impressions if enabled
|
|
322
|
-
if (this.clientConfig.autoTrackImpressions !== false &&
|
|
379
|
+
if (this.clientConfig.autoTrackImpressions !== false &&
|
|
380
|
+
results.results &&
|
|
381
|
+
results.results.length > 0) {
|
|
323
382
|
const delay = this.clientConfig.impressionTrackingDelay || 0;
|
|
324
383
|
const trackImpressions = () => {
|
|
325
384
|
this.autoTrackSearchImpressions(results, context).catch((err) => {
|
|
326
385
|
// Log but don't fail the search if tracking fails
|
|
327
|
-
this.logger.warn(
|
|
386
|
+
this.logger.warn("Failed to auto-track impressions", {
|
|
387
|
+
error: err.message,
|
|
388
|
+
});
|
|
328
389
|
});
|
|
329
390
|
};
|
|
330
391
|
if (delay > 0) {
|
|
@@ -356,8 +417,8 @@ class SeekoraClient {
|
|
|
356
417
|
method: error.config?.method,
|
|
357
418
|
};
|
|
358
419
|
}
|
|
359
|
-
this.logger.error(
|
|
360
|
-
throw this.handleError(error,
|
|
420
|
+
this.logger.error("Search failed", errorDetails);
|
|
421
|
+
throw this.handleError(error, "search");
|
|
361
422
|
}
|
|
362
423
|
}
|
|
363
424
|
/**
|
|
@@ -372,18 +433,18 @@ class SeekoraClient {
|
|
|
372
433
|
*/
|
|
373
434
|
async multiSearch(queries) {
|
|
374
435
|
if (!queries || queries.length === 0) {
|
|
375
|
-
throw new Error(
|
|
436
|
+
throw new Error("At least one search query is required");
|
|
376
437
|
}
|
|
377
438
|
if (queries.length > 10) {
|
|
378
|
-
throw new Error(
|
|
439
|
+
throw new Error("Maximum 10 search queries per request");
|
|
379
440
|
}
|
|
380
|
-
this.logger.verbose(
|
|
441
|
+
this.logger.verbose("Executing multi-search", {
|
|
381
442
|
queryCount: queries.length,
|
|
382
|
-
queries: queries.map(q => q.q),
|
|
443
|
+
queries: queries.map((q) => q.q),
|
|
383
444
|
});
|
|
384
445
|
// Build request body matching DataTypes.MultiSearchRequest
|
|
385
|
-
const searches = queries.map(q => ({
|
|
386
|
-
q: q.q.trim() ||
|
|
446
|
+
const searches = queries.map((q) => ({
|
|
447
|
+
q: q.q.trim() || "*",
|
|
387
448
|
per_page: q.per_page || 10,
|
|
388
449
|
page: q.page || 1,
|
|
389
450
|
filter: q.filter || q.filter_by,
|
|
@@ -403,15 +464,15 @@ class SeekoraClient {
|
|
|
403
464
|
max_facet_values: q.max_facet_values,
|
|
404
465
|
}));
|
|
405
466
|
const headers = {
|
|
406
|
-
|
|
407
|
-
|
|
467
|
+
"x-storeid": this.storeId,
|
|
468
|
+
"x-storesecret": this.readSecret,
|
|
408
469
|
};
|
|
409
470
|
if (this.userId)
|
|
410
|
-
headers[
|
|
471
|
+
headers["x-user-id"] = this.userId;
|
|
411
472
|
if (this.anonId)
|
|
412
|
-
headers[
|
|
473
|
+
headers["x-anon-id"] = this.anonId;
|
|
413
474
|
if (this.sessionId)
|
|
414
|
-
headers[
|
|
475
|
+
headers["x-session-id"] = this.sessionId;
|
|
415
476
|
try {
|
|
416
477
|
const response = await this.searchApi.v1MultiSearchPost(this.storeId, this.readSecret, { searches }, { headers });
|
|
417
478
|
const data = response.data;
|
|
@@ -434,8 +495,12 @@ class SeekoraClient {
|
|
|
434
495
|
const facetsMap = {};
|
|
435
496
|
if (Array.isArray(facets)) {
|
|
436
497
|
facets.forEach((facet, index) => {
|
|
437
|
-
const fieldName = facet?.field_name ||
|
|
438
|
-
|
|
498
|
+
const fieldName = facet?.field_name ||
|
|
499
|
+
facet?.fieldName ||
|
|
500
|
+
facet?.name ||
|
|
501
|
+
`facet_${index}`;
|
|
502
|
+
if (fieldName &&
|
|
503
|
+
(facet?.counts || facet?.values || Array.isArray(facet))) {
|
|
439
504
|
facetsMap[fieldName] = {
|
|
440
505
|
field_name: fieldName,
|
|
441
506
|
counts: facet.counts || facet.values || [],
|
|
@@ -456,33 +521,39 @@ class SeekoraClient {
|
|
|
456
521
|
// Auto-track search event per sub-query
|
|
457
522
|
if (this.autoTrackSearch) {
|
|
458
523
|
this.trackSearch({
|
|
459
|
-
query: queries[i]?.q ||
|
|
460
|
-
|
|
461
|
-
context,
|
|
524
|
+
query: queries[i]?.q || "*",
|
|
525
|
+
numFound: result.totalResults,
|
|
462
526
|
}).catch((err) => {
|
|
463
|
-
this.logger.warn(
|
|
527
|
+
this.logger.warn("Failed to auto-track multi-search event", {
|
|
528
|
+
index: i,
|
|
529
|
+
error: err.message,
|
|
530
|
+
});
|
|
464
531
|
});
|
|
465
532
|
}
|
|
466
533
|
// Auto-track impressions per sub-query
|
|
467
|
-
if (this.clientConfig.autoTrackImpressions !== false &&
|
|
534
|
+
if (this.clientConfig.autoTrackImpressions !== false &&
|
|
535
|
+
result.results.length > 0) {
|
|
468
536
|
this.autoTrackSearchImpressions(result, context).catch((err) => {
|
|
469
|
-
this.logger.warn(
|
|
537
|
+
this.logger.warn("Failed to auto-track multi-search impressions", {
|
|
538
|
+
index: i,
|
|
539
|
+
error: err.message,
|
|
540
|
+
});
|
|
470
541
|
});
|
|
471
542
|
}
|
|
472
543
|
}
|
|
473
|
-
this.logger.info(
|
|
544
|
+
this.logger.info("Multi-search completed", {
|
|
474
545
|
queryCount: queries.length,
|
|
475
|
-
totalResults: resultsList.map(r => r.totalResults),
|
|
546
|
+
totalResults: resultsList.map((r) => r.totalResults),
|
|
476
547
|
});
|
|
477
548
|
return { results: resultsList };
|
|
478
549
|
}
|
|
479
550
|
catch (error) {
|
|
480
|
-
this.logger.error(
|
|
551
|
+
this.logger.error("Multi-search failed", {
|
|
481
552
|
queryCount: queries.length,
|
|
482
553
|
error: error.message,
|
|
483
554
|
status: error.response?.status,
|
|
484
555
|
});
|
|
485
|
-
throw this.handleError(error,
|
|
556
|
+
throw this.handleError(error, "multiSearch");
|
|
486
557
|
}
|
|
487
558
|
}
|
|
488
559
|
/**
|
|
@@ -502,26 +573,26 @@ class SeekoraClient {
|
|
|
502
573
|
* @returns Suggestion hits array, or QuerySuggestionsFullResponse when returnFullResponse is true
|
|
503
574
|
*/
|
|
504
575
|
async getSuggestions(query, options) {
|
|
505
|
-
this.logger.verbose(
|
|
576
|
+
this.logger.verbose("Getting query suggestions", { query, options });
|
|
506
577
|
try {
|
|
507
578
|
const headers = {
|
|
508
|
-
|
|
509
|
-
|
|
579
|
+
"x-storeid": this.storeId,
|
|
580
|
+
"x-storesecret": this.readSecret,
|
|
510
581
|
};
|
|
511
582
|
if (this.userId)
|
|
512
|
-
headers[
|
|
583
|
+
headers["x-user-id"] = this.userId;
|
|
513
584
|
if (this.anonId)
|
|
514
|
-
headers[
|
|
585
|
+
headers["x-anon-id"] = this.anonId;
|
|
515
586
|
if (this.sessionId)
|
|
516
|
-
headers[
|
|
587
|
+
headers["x-session-id"] = this.sessionId;
|
|
517
588
|
const defaults = this.clientConfig?.suggestionsDefaults;
|
|
518
|
-
this.logger.verbose(
|
|
519
|
-
endpoint:
|
|
589
|
+
this.logger.verbose("Using GET endpoint", {
|
|
590
|
+
endpoint: "/api/v1/suggestions/queries",
|
|
520
591
|
query,
|
|
521
592
|
hasFilteredTabs: !!(options?.filtered_tabs && options.filtered_tabs.length > 0),
|
|
522
593
|
});
|
|
523
594
|
const analyticsTags = Array.isArray(options?.analytics_tags)
|
|
524
|
-
? options.analytics_tags.join(
|
|
595
|
+
? options.analytics_tags.join(",")
|
|
525
596
|
: options?.analytics_tags;
|
|
526
597
|
// Encode filtered_tabs as JSON string for URL parameter
|
|
527
598
|
const filteredTabsParam = options?.filtered_tabs
|
|
@@ -529,7 +600,9 @@ class SeekoraClient {
|
|
|
529
600
|
: undefined;
|
|
530
601
|
const response = await this.suggestionsApi.v1SuggestionsQueriesGet(this.storeId, this.readSecret, this.userId, this.anonId, this.sessionId, query, // query parameter
|
|
531
602
|
undefined, // q parameter (alias for query)
|
|
532
|
-
options?.hitsPerPage ?? defaults?.hitsPerPage ?? 5, options?.page, analyticsTags, options?.tags_match_mode, options?.include_categories, options?.include_facets, options?.include_dropdown_recommendations ??
|
|
603
|
+
options?.hitsPerPage ?? defaults?.hitsPerPage ?? 5, options?.page, analyticsTags, options?.tags_match_mode, options?.include_categories, options?.include_facets, options?.include_dropdown_recommendations ??
|
|
604
|
+
defaults?.include_dropdown_recommendations, options?.include_dropdown_product_list ??
|
|
605
|
+
defaults?.include_dropdown_product_list, options?.include_filtered_tabs ?? defaults?.include_filtered_tabs, undefined, // include_empty_query_recommendations
|
|
533
606
|
options?.max_categories, options?.max_facets, options?.min_popularity, (options?.time_range ?? defaults?.time_range), options?.disable_typo_tolerance, filteredTabsParam, // filtered_tabs as JSON string
|
|
534
607
|
undefined, // userId (using header instead)
|
|
535
608
|
undefined, // anonId (using header instead)
|
|
@@ -538,8 +611,9 @@ class SeekoraClient {
|
|
|
538
611
|
const responseData = response.data?.data || response.data;
|
|
539
612
|
const suggestions = responseData?.results?.[0]?.hits || responseData?.hits || [];
|
|
540
613
|
if (options?.returnFullResponse) {
|
|
541
|
-
const extensions = responseData?.results?.[1]?.extensions ??
|
|
542
|
-
|
|
614
|
+
const extensions = responseData?.results?.[1]?.extensions ??
|
|
615
|
+
responseData?.results?.[0]?.extensions;
|
|
616
|
+
this.logger.info("Query suggestions retrieved (GET, full response)", {
|
|
543
617
|
query,
|
|
544
618
|
count: suggestions.length,
|
|
545
619
|
status: response.status,
|
|
@@ -551,7 +625,7 @@ class SeekoraClient {
|
|
|
551
625
|
raw: responseData,
|
|
552
626
|
};
|
|
553
627
|
}
|
|
554
|
-
this.logger.info(
|
|
628
|
+
this.logger.info("Query suggestions retrieved (GET)", {
|
|
555
629
|
query,
|
|
556
630
|
count: suggestions.length,
|
|
557
631
|
status: response.status,
|
|
@@ -559,8 +633,11 @@ class SeekoraClient {
|
|
|
559
633
|
return suggestions;
|
|
560
634
|
}
|
|
561
635
|
catch (error) {
|
|
562
|
-
this.logger.error(
|
|
563
|
-
|
|
636
|
+
this.logger.error("Failed to get suggestions", {
|
|
637
|
+
query,
|
|
638
|
+
error: error.message,
|
|
639
|
+
});
|
|
640
|
+
throw this.handleError(error, "getSuggestions");
|
|
564
641
|
}
|
|
565
642
|
}
|
|
566
643
|
/**
|
|
@@ -568,27 +645,27 @@ class SeekoraClient {
|
|
|
568
645
|
* Uses GET /api/v1/stores/{storeId}/query-suggestions/config via StoreManagementApi.
|
|
569
646
|
*/
|
|
570
647
|
async getSuggestionsConfig() {
|
|
571
|
-
this.logger.verbose(
|
|
648
|
+
this.logger.verbose("Getting suggestions configuration");
|
|
572
649
|
try {
|
|
573
650
|
const headers = {
|
|
574
|
-
|
|
575
|
-
|
|
651
|
+
"x-storeid": this.storeId,
|
|
652
|
+
"x-storesecret": this.readSecret,
|
|
576
653
|
};
|
|
577
654
|
try {
|
|
578
655
|
const response = await this.storeManagementApi.apiV1StoresQuerySuggestionsConfigGet(this.storeId, this.readSecret, { headers });
|
|
579
656
|
const data = response.data?.data;
|
|
580
657
|
const config = data?.metadata?.config ?? data?.config ?? data;
|
|
581
|
-
this.logger.info(
|
|
658
|
+
this.logger.info("Suggestions configuration retrieved", {
|
|
582
659
|
configKeys: config ? Object.keys(config) : [],
|
|
583
660
|
status: response.status,
|
|
584
661
|
});
|
|
585
662
|
return config;
|
|
586
663
|
}
|
|
587
664
|
catch (e) {
|
|
588
|
-
this.logger.verbose(
|
|
665
|
+
this.logger.verbose("Store config endpoint failed, trying legacy endpoint", { message: e?.message });
|
|
589
666
|
const response = await this.suggestionsApi.v1SuggestionsConfigGet(this.storeId, this.readSecret, { headers });
|
|
590
667
|
const config = response.data?.data;
|
|
591
|
-
this.logger.info(
|
|
668
|
+
this.logger.info("Suggestions configuration retrieved (legacy)", {
|
|
592
669
|
configKeys: config ? Object.keys(config) : [],
|
|
593
670
|
status: response.status,
|
|
594
671
|
});
|
|
@@ -596,8 +673,10 @@ class SeekoraClient {
|
|
|
596
673
|
}
|
|
597
674
|
}
|
|
598
675
|
catch (error) {
|
|
599
|
-
this.logger.error(
|
|
600
|
-
|
|
676
|
+
this.logger.error("Failed to get suggestions config", {
|
|
677
|
+
error: error.message,
|
|
678
|
+
});
|
|
679
|
+
throw this.handleError(error, "getSuggestionsConfig");
|
|
601
680
|
}
|
|
602
681
|
}
|
|
603
682
|
/**
|
|
@@ -605,37 +684,37 @@ class SeekoraClient {
|
|
|
605
684
|
* Returns store search configuration and onboarding status
|
|
606
685
|
*/
|
|
607
686
|
async getConfig() {
|
|
608
|
-
this.logger.verbose(
|
|
687
|
+
this.logger.verbose("Getting store configuration");
|
|
609
688
|
try {
|
|
610
|
-
this.logger.verbose(
|
|
611
|
-
endpoint: `/api/v1/stores/${this.storeId}/config
|
|
689
|
+
this.logger.verbose("Fetching store configuration", {
|
|
690
|
+
endpoint: `/api/v1/stores/${this.storeId}/config`,
|
|
612
691
|
});
|
|
613
692
|
// Build headers with personalization support
|
|
614
693
|
const headers = {
|
|
615
|
-
|
|
616
|
-
|
|
694
|
+
"x-storeid": this.storeId,
|
|
695
|
+
"x-storesecret": this.readSecret,
|
|
617
696
|
};
|
|
618
697
|
// Add personalization headers if available
|
|
619
698
|
if (this.userId) {
|
|
620
|
-
headers[
|
|
699
|
+
headers["x-user-id"] = this.userId;
|
|
621
700
|
}
|
|
622
701
|
if (this.anonId) {
|
|
623
|
-
headers[
|
|
702
|
+
headers["x-anon-id"] = this.anonId;
|
|
624
703
|
}
|
|
625
704
|
if (this.sessionId) {
|
|
626
|
-
headers[
|
|
705
|
+
headers["x-session-id"] = this.sessionId;
|
|
627
706
|
}
|
|
628
707
|
const response = await this.storeManagementApi.apiV1StoresConfigGet(this.storeId, this.readSecret, { headers });
|
|
629
708
|
const config = response.data?.data;
|
|
630
|
-
this.logger.info(
|
|
709
|
+
this.logger.info("Store configuration retrieved", {
|
|
631
710
|
status: response.status,
|
|
632
|
-
hasConfig: !!config
|
|
711
|
+
hasConfig: !!config,
|
|
633
712
|
});
|
|
634
713
|
return config;
|
|
635
714
|
}
|
|
636
715
|
catch (error) {
|
|
637
|
-
this.logger.error(
|
|
638
|
-
throw this.handleError(error,
|
|
716
|
+
this.logger.error("Failed to get store config", { error: error.message });
|
|
717
|
+
throw this.handleError(error, "getConfig");
|
|
639
718
|
}
|
|
640
719
|
}
|
|
641
720
|
/**
|
|
@@ -648,7 +727,7 @@ class SeekoraClient {
|
|
|
648
727
|
* @returns Store information object
|
|
649
728
|
*/
|
|
650
729
|
async getStoreInfo() {
|
|
651
|
-
this.logger.verbose(
|
|
730
|
+
this.logger.verbose("Getting store information");
|
|
652
731
|
try {
|
|
653
732
|
const configResponse = await this.getConfig();
|
|
654
733
|
// Extract store info from config response
|
|
@@ -690,51 +769,55 @@ class SeekoraClient {
|
|
|
690
769
|
storeInfo.config = config;
|
|
691
770
|
}
|
|
692
771
|
}
|
|
693
|
-
this.logger.info(
|
|
772
|
+
this.logger.info("Store information retrieved", {
|
|
694
773
|
storeId: storeInfo.storeId,
|
|
695
|
-
storeName: storeInfo.storeName
|
|
774
|
+
storeName: storeInfo.storeName,
|
|
696
775
|
});
|
|
697
776
|
return storeInfo;
|
|
698
777
|
}
|
|
699
778
|
catch (error) {
|
|
700
|
-
this.logger.error(
|
|
701
|
-
throw this.handleError(error,
|
|
779
|
+
this.logger.error("Failed to get store info", { error: error.message });
|
|
780
|
+
throw this.handleError(error, "getStoreInfo");
|
|
702
781
|
}
|
|
703
782
|
}
|
|
704
783
|
/**
|
|
705
784
|
* Update store configuration (requires write secret)
|
|
706
785
|
*/
|
|
707
786
|
async updateConfig(config) {
|
|
708
|
-
this.logger.verbose(
|
|
787
|
+
this.logger.verbose("Updating store configuration", {
|
|
788
|
+
configKeys: Object.keys(config),
|
|
789
|
+
});
|
|
709
790
|
if (!this.writeSecret) {
|
|
710
|
-
this.logger.error(
|
|
711
|
-
operation:
|
|
712
|
-
storeId: this.storeId
|
|
791
|
+
this.logger.error("Write secret required but not provided", {
|
|
792
|
+
operation: "updateConfig",
|
|
793
|
+
storeId: this.storeId,
|
|
713
794
|
});
|
|
714
|
-
throw new Error(
|
|
795
|
+
throw new Error("Write secret is required for updateConfig");
|
|
715
796
|
}
|
|
716
797
|
try {
|
|
717
|
-
this.logger.verbose(
|
|
798
|
+
this.logger.verbose("Updating store configuration", {
|
|
718
799
|
endpoint: `/api/v1/stores/${this.storeId}/config`,
|
|
719
|
-
configKeys: Object.keys(config)
|
|
800
|
+
configKeys: Object.keys(config),
|
|
720
801
|
});
|
|
721
802
|
// Build headers with write secret
|
|
722
803
|
const headers = {
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
804
|
+
"x-storeid": this.storeId,
|
|
805
|
+
"x-storesecret": this.readSecret,
|
|
806
|
+
"x-store-write-secret": this.writeSecret,
|
|
726
807
|
};
|
|
727
808
|
const response = await this.storeManagementApi.apiV1StoresConfigPut(this.storeId, this.writeSecret, config, { headers });
|
|
728
809
|
const updatedConfig = response.data?.data;
|
|
729
|
-
this.logger.info(
|
|
810
|
+
this.logger.info("Store configuration updated successfully", {
|
|
730
811
|
status: response.status,
|
|
731
|
-
hasConfig: !!updatedConfig
|
|
812
|
+
hasConfig: !!updatedConfig,
|
|
732
813
|
});
|
|
733
814
|
return updatedConfig;
|
|
734
815
|
}
|
|
735
816
|
catch (error) {
|
|
736
|
-
this.logger.error(
|
|
737
|
-
|
|
817
|
+
this.logger.error("Failed to update store config", {
|
|
818
|
+
error: error.message,
|
|
819
|
+
});
|
|
820
|
+
throw this.handleError(error, "updateConfig");
|
|
738
821
|
}
|
|
739
822
|
}
|
|
740
823
|
/**
|
|
@@ -742,38 +825,44 @@ class SeekoraClient {
|
|
|
742
825
|
* Uses PUT /api/v1/stores/{xStoreID}/query-suggestions/config via StoreManagementApi.
|
|
743
826
|
*/
|
|
744
827
|
async updateQuerySuggestionsConfig(config) {
|
|
745
|
-
this.logger.verbose(
|
|
828
|
+
this.logger.verbose("Updating query suggestions configuration", {
|
|
829
|
+
configKeys: Object.keys(config),
|
|
830
|
+
});
|
|
746
831
|
if (!this.writeSecret) {
|
|
747
|
-
this.logger.error(
|
|
748
|
-
operation:
|
|
832
|
+
this.logger.error("Write secret required but not provided", {
|
|
833
|
+
operation: "updateQuerySuggestionsConfig",
|
|
749
834
|
storeId: this.storeId,
|
|
750
835
|
});
|
|
751
|
-
throw new Error(
|
|
836
|
+
throw new Error("Write secret is required for updateQuerySuggestionsConfig");
|
|
752
837
|
}
|
|
753
838
|
try {
|
|
754
839
|
const headers = {
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
840
|
+
"x-storeid": this.storeId,
|
|
841
|
+
"x-store-write-secret": this.writeSecret,
|
|
842
|
+
"Content-Type": "application/json",
|
|
758
843
|
};
|
|
759
844
|
try {
|
|
760
845
|
const response = await this.storeManagementApi.apiV1StoresQuerySuggestionsConfigPut(this.storeId, this.writeSecret, config, { headers });
|
|
761
846
|
const data = response.data?.data;
|
|
762
847
|
const updatedConfig = data?.metadata?.config ?? data?.config ?? data;
|
|
763
|
-
this.logger.info(
|
|
848
|
+
this.logger.info("Query suggestions configuration updated successfully", {
|
|
764
849
|
status: response.status,
|
|
765
850
|
hasConfig: !!updatedConfig,
|
|
766
851
|
});
|
|
767
852
|
return updatedConfig;
|
|
768
853
|
}
|
|
769
854
|
catch (apiError) {
|
|
770
|
-
this.logger.verbose(
|
|
771
|
-
|
|
772
|
-
|
|
855
|
+
this.logger.verbose("PUT via generated API failed, using direct HTTP", {
|
|
856
|
+
message: apiError?.message,
|
|
857
|
+
});
|
|
858
|
+
const axiosInstance = this.storeManagementApi.axios ??
|
|
859
|
+
this.suggestionsApi.axios ??
|
|
860
|
+
axios_1.default;
|
|
861
|
+
const baseUrl = this.config.basePath ?? "https://api.seekora.com/api";
|
|
773
862
|
const response = await axiosInstance.put(`${baseUrl}/v1/stores/${this.storeId}/query-suggestions/config`, config, { headers });
|
|
774
863
|
const data = response.data?.data;
|
|
775
864
|
const updatedConfig = data?.metadata?.config ?? data?.config ?? data;
|
|
776
|
-
this.logger.info(
|
|
865
|
+
this.logger.info("Query suggestions configuration updated successfully", {
|
|
777
866
|
status: response.status,
|
|
778
867
|
hasConfig: !!updatedConfig,
|
|
779
868
|
});
|
|
@@ -781,29 +870,35 @@ class SeekoraClient {
|
|
|
781
870
|
}
|
|
782
871
|
}
|
|
783
872
|
catch (error) {
|
|
784
|
-
this.logger.error(
|
|
785
|
-
|
|
873
|
+
this.logger.error("Failed to update query suggestions config", {
|
|
874
|
+
error: error.message,
|
|
875
|
+
});
|
|
876
|
+
throw this.handleError(error, "updateQuerySuggestionsConfig");
|
|
786
877
|
}
|
|
787
878
|
}
|
|
788
879
|
/**
|
|
789
880
|
* Get configuration schema
|
|
790
881
|
*/
|
|
791
882
|
async getConfigSchema() {
|
|
792
|
-
this.logger.verbose(
|
|
883
|
+
this.logger.verbose("Getting configuration schema");
|
|
793
884
|
try {
|
|
794
885
|
const response = await this.storeManagementApi.apiV1StoresConfigSchemaGet(this.storeId, this.readSecret, {
|
|
795
886
|
headers: {
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
}
|
|
887
|
+
"x-storeid": this.storeId,
|
|
888
|
+
"x-storesecret": this.readSecret,
|
|
889
|
+
},
|
|
799
890
|
});
|
|
800
891
|
const schema = response.data?.data;
|
|
801
|
-
this.logger.verbose(
|
|
892
|
+
this.logger.verbose("Configuration schema retrieved", {
|
|
893
|
+
schemaKeys: schema ? Object.keys(schema) : [],
|
|
894
|
+
});
|
|
802
895
|
return schema;
|
|
803
896
|
}
|
|
804
897
|
catch (error) {
|
|
805
|
-
this.logger.error(
|
|
806
|
-
|
|
898
|
+
this.logger.error("Failed to get config schema", {
|
|
899
|
+
error: error.message,
|
|
900
|
+
});
|
|
901
|
+
throw this.handleError(error, "getConfigSchema");
|
|
807
902
|
}
|
|
808
903
|
}
|
|
809
904
|
// ==========================================
|
|
@@ -816,16 +911,16 @@ class SeekoraClient {
|
|
|
816
911
|
* @returns IndexDocumentResponse with document id and status
|
|
817
912
|
*/
|
|
818
913
|
async indexDocument(document) {
|
|
819
|
-
this.logger.verbose(
|
|
914
|
+
this.logger.verbose("Indexing document", {
|
|
820
915
|
hasCustomId: !!document.id,
|
|
821
|
-
documentKeys: Object.keys(document.data)
|
|
916
|
+
documentKeys: Object.keys(document.data),
|
|
822
917
|
});
|
|
823
918
|
if (!this.writeSecret) {
|
|
824
|
-
this.logger.error(
|
|
825
|
-
operation:
|
|
826
|
-
storeId: this.storeId
|
|
919
|
+
this.logger.error("Write secret required but not provided", {
|
|
920
|
+
operation: "indexDocument",
|
|
921
|
+
storeId: this.storeId,
|
|
827
922
|
});
|
|
828
|
-
throw new Error(
|
|
923
|
+
throw new Error("Write secret is required for indexDocument");
|
|
829
924
|
}
|
|
830
925
|
try {
|
|
831
926
|
const request = {
|
|
@@ -834,20 +929,23 @@ class SeekoraClient {
|
|
|
834
929
|
};
|
|
835
930
|
const response = await this.storeManagementApi.apiV1StoresDocumentsPost(this.storeId, this.writeSecret, request);
|
|
836
931
|
const responseData = response.data?.data;
|
|
837
|
-
this.logger.info(
|
|
932
|
+
this.logger.info("Document indexed successfully", {
|
|
838
933
|
id: responseData?.id || document.id,
|
|
839
|
-
status: response.status
|
|
934
|
+
status: response.status,
|
|
840
935
|
});
|
|
841
936
|
return {
|
|
842
|
-
id: responseData?.id || document.id ||
|
|
843
|
-
success: responseData?.status ===
|
|
937
|
+
id: responseData?.id || document.id || "",
|
|
938
|
+
success: responseData?.status === "success" ||
|
|
939
|
+
responseData?.status === "inserted" ||
|
|
940
|
+
responseData?.status === "updated" ||
|
|
941
|
+
true,
|
|
844
942
|
message: responseData?.message,
|
|
845
943
|
data: responseData,
|
|
846
944
|
};
|
|
847
945
|
}
|
|
848
946
|
catch (error) {
|
|
849
|
-
this.logger.error(
|
|
850
|
-
throw this.handleError(error,
|
|
947
|
+
this.logger.error("Failed to index document", { error: error.message });
|
|
948
|
+
throw this.handleError(error, "indexDocument");
|
|
851
949
|
}
|
|
852
950
|
}
|
|
853
951
|
/**
|
|
@@ -857,20 +955,20 @@ class SeekoraClient {
|
|
|
857
955
|
* @returns BulkIndexResponse with success/error counts
|
|
858
956
|
*/
|
|
859
957
|
async indexDocuments(documents) {
|
|
860
|
-
this.logger.verbose(
|
|
958
|
+
this.logger.verbose("Bulk indexing documents", {
|
|
861
959
|
count: documents.length,
|
|
862
|
-
actions: documents.map(d => d.action ||
|
|
960
|
+
actions: documents.map((d) => d.action || "upsert"),
|
|
863
961
|
});
|
|
864
962
|
if (!this.writeSecret) {
|
|
865
|
-
this.logger.error(
|
|
866
|
-
operation:
|
|
867
|
-
storeId: this.storeId
|
|
963
|
+
this.logger.error("Write secret required but not provided", {
|
|
964
|
+
operation: "indexDocuments",
|
|
965
|
+
storeId: this.storeId,
|
|
868
966
|
});
|
|
869
|
-
throw new Error(
|
|
967
|
+
throw new Error("Write secret is required for indexDocuments");
|
|
870
968
|
}
|
|
871
969
|
try {
|
|
872
970
|
const request = {
|
|
873
|
-
documents: documents.map(doc => ({
|
|
971
|
+
documents: documents.map((doc) => ({
|
|
874
972
|
id: doc.id,
|
|
875
973
|
action: doc.action,
|
|
876
974
|
data: doc.data,
|
|
@@ -878,31 +976,35 @@ class SeekoraClient {
|
|
|
878
976
|
};
|
|
879
977
|
const response = await this.storeManagementApi.apiV1StoresDocumentsBulkPost(this.storeId, this.writeSecret, request);
|
|
880
978
|
const responseData = response.data?.data;
|
|
881
|
-
this.logger.info(
|
|
979
|
+
this.logger.info("Bulk document indexing completed", {
|
|
882
980
|
successCount: responseData?.successCount || 0,
|
|
883
981
|
errorCount: responseData?.errorCount || 0,
|
|
884
|
-
status: response.status
|
|
982
|
+
status: response.status,
|
|
885
983
|
});
|
|
886
984
|
return {
|
|
887
985
|
success_count: responseData?.successCount || documents.length,
|
|
888
986
|
error_count: responseData?.errorCount || 0,
|
|
889
|
-
results: responseData?.results?.map(r => ({
|
|
987
|
+
results: responseData?.results?.map((r) => ({
|
|
890
988
|
id: r.id,
|
|
891
|
-
success: r.status ===
|
|
989
|
+
success: r.status === "success" ||
|
|
990
|
+
r.status === "inserted" ||
|
|
991
|
+
r.status === "updated" ||
|
|
992
|
+
!r.error,
|
|
892
993
|
error: r.error,
|
|
893
|
-
})) ||
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
994
|
+
})) ||
|
|
995
|
+
documents.map((doc) => ({
|
|
996
|
+
id: doc.id,
|
|
997
|
+
success: true,
|
|
998
|
+
})),
|
|
897
999
|
data: responseData,
|
|
898
1000
|
};
|
|
899
1001
|
}
|
|
900
1002
|
catch (error) {
|
|
901
|
-
this.logger.error(
|
|
1003
|
+
this.logger.error("Failed to bulk index documents", {
|
|
902
1004
|
count: documents.length,
|
|
903
|
-
error: error.message
|
|
1005
|
+
error: error.message,
|
|
904
1006
|
});
|
|
905
|
-
throw this.handleError(error,
|
|
1007
|
+
throw this.handleError(error, "indexDocuments");
|
|
906
1008
|
}
|
|
907
1009
|
}
|
|
908
1010
|
/**
|
|
@@ -911,27 +1013,27 @@ class SeekoraClient {
|
|
|
911
1013
|
* @param documentId - Document ID to delete
|
|
912
1014
|
*/
|
|
913
1015
|
async deleteDocument(documentId) {
|
|
914
|
-
this.logger.verbose(
|
|
1016
|
+
this.logger.verbose("Deleting document", { documentId });
|
|
915
1017
|
if (!this.writeSecret) {
|
|
916
|
-
this.logger.error(
|
|
917
|
-
operation:
|
|
918
|
-
storeId: this.storeId
|
|
1018
|
+
this.logger.error("Write secret required but not provided", {
|
|
1019
|
+
operation: "deleteDocument",
|
|
1020
|
+
storeId: this.storeId,
|
|
919
1021
|
});
|
|
920
|
-
throw new Error(
|
|
1022
|
+
throw new Error("Write secret is required for deleteDocument");
|
|
921
1023
|
}
|
|
922
1024
|
try {
|
|
923
1025
|
const response = await this.storeManagementApi.apiV1StoresDocumentsDocumentIDDelete(this.storeId, this.writeSecret, documentId);
|
|
924
|
-
this.logger.info(
|
|
1026
|
+
this.logger.info("Document deleted successfully", {
|
|
925
1027
|
documentId,
|
|
926
|
-
status: response.status
|
|
1028
|
+
status: response.status,
|
|
927
1029
|
});
|
|
928
1030
|
}
|
|
929
1031
|
catch (error) {
|
|
930
|
-
this.logger.error(
|
|
1032
|
+
this.logger.error("Failed to delete document", {
|
|
931
1033
|
documentId,
|
|
932
|
-
error: error.message
|
|
1034
|
+
error: error.message,
|
|
933
1035
|
});
|
|
934
|
-
throw this.handleError(error,
|
|
1036
|
+
throw this.handleError(error, "deleteDocument");
|
|
935
1037
|
}
|
|
936
1038
|
}
|
|
937
1039
|
// ==================
|
|
@@ -970,20 +1072,20 @@ class SeekoraClient {
|
|
|
970
1072
|
* });
|
|
971
1073
|
*/
|
|
972
1074
|
async createSchema(request) {
|
|
973
|
-
this.logger.verbose(
|
|
1075
|
+
this.logger.verbose("Creating/updating schema", {
|
|
974
1076
|
fieldCount: request.fields.length,
|
|
975
|
-
mode: request.mode ||
|
|
1077
|
+
mode: request.mode || "additive",
|
|
976
1078
|
});
|
|
977
1079
|
if (!this.writeSecret) {
|
|
978
|
-
this.logger.error(
|
|
979
|
-
operation:
|
|
980
|
-
storeId: this.storeId
|
|
1080
|
+
this.logger.error("Write secret required but not provided", {
|
|
1081
|
+
operation: "createSchema",
|
|
1082
|
+
storeId: this.storeId,
|
|
981
1083
|
});
|
|
982
|
-
throw new Error(
|
|
1084
|
+
throw new Error("Write secret is required for createSchema");
|
|
983
1085
|
}
|
|
984
1086
|
try {
|
|
985
1087
|
const apiRequest = {
|
|
986
|
-
fields: request.fields.map(f => ({
|
|
1088
|
+
fields: request.fields.map((f) => ({
|
|
987
1089
|
name: f.name,
|
|
988
1090
|
type: f.type,
|
|
989
1091
|
facet: f.facet,
|
|
@@ -1000,15 +1102,15 @@ class SeekoraClient {
|
|
|
1000
1102
|
};
|
|
1001
1103
|
const response = await this.storeManagementApi.apiV1StoresSchemaPost(this.storeId, this.writeSecret, apiRequest);
|
|
1002
1104
|
const responseData = response.data;
|
|
1003
|
-
this.logger.info(
|
|
1105
|
+
this.logger.info("Schema created/updated successfully", {
|
|
1004
1106
|
name: responseData.data?.name,
|
|
1005
|
-
fieldCount: responseData.data?.fields?.length
|
|
1107
|
+
fieldCount: responseData.data?.fields?.length,
|
|
1006
1108
|
});
|
|
1007
1109
|
return {
|
|
1008
|
-
name: responseData.data?.name ||
|
|
1009
|
-
fields: (responseData.data?.fields || []).map(f => ({
|
|
1010
|
-
name: f.name ||
|
|
1011
|
-
type: f.type ||
|
|
1110
|
+
name: responseData.data?.name || "",
|
|
1111
|
+
fields: (responseData.data?.fields || []).map((f) => ({
|
|
1112
|
+
name: f.name || "",
|
|
1113
|
+
type: f.type || "string",
|
|
1012
1114
|
facet: f.facet,
|
|
1013
1115
|
index: f.index,
|
|
1014
1116
|
optional: f.optional,
|
|
@@ -1022,8 +1124,10 @@ class SeekoraClient {
|
|
|
1022
1124
|
};
|
|
1023
1125
|
}
|
|
1024
1126
|
catch (error) {
|
|
1025
|
-
this.logger.error(
|
|
1026
|
-
|
|
1127
|
+
this.logger.error("Failed to create/update schema", {
|
|
1128
|
+
error: error.message,
|
|
1129
|
+
});
|
|
1130
|
+
throw this.handleError(error, "createSchema");
|
|
1027
1131
|
}
|
|
1028
1132
|
}
|
|
1029
1133
|
/**
|
|
@@ -1037,20 +1141,20 @@ class SeekoraClient {
|
|
|
1037
1141
|
* console.log('Documents:', schema.numDocuments);
|
|
1038
1142
|
*/
|
|
1039
1143
|
async getSchema() {
|
|
1040
|
-
this.logger.verbose(
|
|
1144
|
+
this.logger.verbose("Getting schema", { storeId: this.storeId });
|
|
1041
1145
|
try {
|
|
1042
1146
|
const response = await this.storeManagementApi.apiV1StoresSchemaGet(this.storeId, this.readSecret);
|
|
1043
1147
|
const responseData = response.data;
|
|
1044
|
-
this.logger.info(
|
|
1148
|
+
this.logger.info("Schema retrieved successfully", {
|
|
1045
1149
|
name: responseData.data?.name,
|
|
1046
1150
|
fieldCount: responseData.data?.fields?.length,
|
|
1047
|
-
numDocuments: responseData.data?.num_documents
|
|
1151
|
+
numDocuments: responseData.data?.num_documents,
|
|
1048
1152
|
});
|
|
1049
1153
|
return {
|
|
1050
|
-
name: responseData.data?.name ||
|
|
1051
|
-
fields: (responseData.data?.fields || []).map(f => ({
|
|
1052
|
-
name: f.name ||
|
|
1053
|
-
type: f.type ||
|
|
1154
|
+
name: responseData.data?.name || "",
|
|
1155
|
+
fields: (responseData.data?.fields || []).map((f) => ({
|
|
1156
|
+
name: f.name || "",
|
|
1157
|
+
type: f.type || "string",
|
|
1054
1158
|
facet: f.facet,
|
|
1055
1159
|
index: f.index,
|
|
1056
1160
|
optional: f.optional,
|
|
@@ -1064,8 +1168,8 @@ class SeekoraClient {
|
|
|
1064
1168
|
};
|
|
1065
1169
|
}
|
|
1066
1170
|
catch (error) {
|
|
1067
|
-
this.logger.error(
|
|
1068
|
-
throw this.handleError(error,
|
|
1171
|
+
this.logger.error("Failed to get schema", { error: error.message });
|
|
1172
|
+
throw this.handleError(error, "getSchema");
|
|
1069
1173
|
}
|
|
1070
1174
|
}
|
|
1071
1175
|
/**
|
|
@@ -1081,28 +1185,28 @@ class SeekoraClient {
|
|
|
1081
1185
|
* console.log(`Cleared ${result.deletedCount} documents`);
|
|
1082
1186
|
*/
|
|
1083
1187
|
async clearDocuments() {
|
|
1084
|
-
this.logger.verbose(
|
|
1188
|
+
this.logger.verbose("Clearing all documents", { storeId: this.storeId });
|
|
1085
1189
|
if (!this.writeSecret) {
|
|
1086
|
-
this.logger.error(
|
|
1087
|
-
operation:
|
|
1088
|
-
storeId: this.storeId
|
|
1190
|
+
this.logger.error("Write secret required but not provided", {
|
|
1191
|
+
operation: "clearDocuments",
|
|
1192
|
+
storeId: this.storeId,
|
|
1089
1193
|
});
|
|
1090
|
-
throw new Error(
|
|
1194
|
+
throw new Error("Write secret is required for clearDocuments");
|
|
1091
1195
|
}
|
|
1092
1196
|
try {
|
|
1093
1197
|
const response = await this.storeManagementApi.apiV1StoresDocumentsDelete(this.storeId, this.writeSecret);
|
|
1094
1198
|
const responseData = response.data;
|
|
1095
|
-
this.logger.info(
|
|
1096
|
-
deletedCount: responseData.data?.deleted_count
|
|
1199
|
+
this.logger.info("Documents cleared successfully", {
|
|
1200
|
+
deletedCount: responseData.data?.deleted_count,
|
|
1097
1201
|
});
|
|
1098
1202
|
return {
|
|
1099
1203
|
deletedCount: responseData.data?.deleted_count || 0,
|
|
1100
|
-
message: responseData.data?.message ||
|
|
1204
|
+
message: responseData.data?.message || "Documents cleared",
|
|
1101
1205
|
};
|
|
1102
1206
|
}
|
|
1103
1207
|
catch (error) {
|
|
1104
|
-
this.logger.error(
|
|
1105
|
-
throw this.handleError(error,
|
|
1208
|
+
this.logger.error("Failed to clear documents", { error: error.message });
|
|
1209
|
+
throw this.handleError(error, "clearDocuments");
|
|
1106
1210
|
}
|
|
1107
1211
|
}
|
|
1108
1212
|
/**
|
|
@@ -1111,14 +1215,16 @@ class SeekoraClient {
|
|
|
1111
1215
|
async collectContextAsync() {
|
|
1112
1216
|
try {
|
|
1113
1217
|
this.cachedBrowserContext = await this.contextCollector.collect();
|
|
1114
|
-
this.logger.verbose(
|
|
1218
|
+
this.logger.verbose("Browser context collected", {
|
|
1115
1219
|
hasFingerprint: !!this.cachedBrowserContext?.device_fingerprint,
|
|
1116
1220
|
platform: this.cachedBrowserContext?.platform,
|
|
1117
1221
|
timezone: this.cachedBrowserContext?.timezone,
|
|
1118
1222
|
});
|
|
1119
1223
|
}
|
|
1120
1224
|
catch (error) {
|
|
1121
|
-
this.logger.warn(
|
|
1225
|
+
this.logger.warn("Failed to collect browser context", {
|
|
1226
|
+
error: error.message,
|
|
1227
|
+
});
|
|
1122
1228
|
}
|
|
1123
1229
|
}
|
|
1124
1230
|
/**
|
|
@@ -1142,9 +1248,9 @@ class SeekoraClient {
|
|
|
1142
1248
|
async sendEventDirect(payload) {
|
|
1143
1249
|
const response = await this.analyticsApi.analyticsEventPost(this.storeId, this.readSecret, payload, {
|
|
1144
1250
|
headers: {
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
}
|
|
1251
|
+
"x-storeid": this.storeId,
|
|
1252
|
+
"x-storesecret": this.readSecret,
|
|
1253
|
+
},
|
|
1148
1254
|
});
|
|
1149
1255
|
if (response.status >= 400) {
|
|
1150
1256
|
throw new Error(`Failed to send event: ${response.status}`);
|
|
@@ -1155,13 +1261,13 @@ class SeekoraClient {
|
|
|
1155
1261
|
*/
|
|
1156
1262
|
async sendEventsBatchDirect(payloads) {
|
|
1157
1263
|
const batchRequest = {
|
|
1158
|
-
events: payloads
|
|
1264
|
+
events: payloads,
|
|
1159
1265
|
};
|
|
1160
1266
|
const response = await this.analyticsApi.analyticsBatchPost(this.storeId, this.readSecret, batchRequest, {
|
|
1161
1267
|
headers: {
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
}
|
|
1268
|
+
"x-storeid": this.storeId,
|
|
1269
|
+
"x-storesecret": this.readSecret,
|
|
1270
|
+
},
|
|
1165
1271
|
});
|
|
1166
1272
|
if (response.status >= 400) {
|
|
1167
1273
|
throw new Error(`Failed to send batch events: ${response.status}`);
|
|
@@ -1176,14 +1282,14 @@ class SeekoraClient {
|
|
|
1176
1282
|
if (items.length === 0) {
|
|
1177
1283
|
return;
|
|
1178
1284
|
}
|
|
1179
|
-
this.logger.verbose(
|
|
1285
|
+
this.logger.verbose("Auto-tracking search impressions", {
|
|
1180
1286
|
itemCount: items.length,
|
|
1181
1287
|
query: searchResponse.query || searchResponse.data?.data?.query,
|
|
1182
|
-
searchId: context.searchId
|
|
1288
|
+
searchId: context.searchId,
|
|
1183
1289
|
});
|
|
1184
1290
|
// Create batch impression events
|
|
1185
1291
|
const impressionEvents = items.map((item, index) => ({
|
|
1186
|
-
event_name:
|
|
1292
|
+
event_name: "view",
|
|
1187
1293
|
event_id: (0, utils_1.generateUUID)(),
|
|
1188
1294
|
event_ts: new Date().toISOString(),
|
|
1189
1295
|
// Link to search
|
|
@@ -1199,16 +1305,18 @@ class SeekoraClient {
|
|
|
1199
1305
|
position: index + 1,
|
|
1200
1306
|
query: searchResponse.query || searchResponse.data?.data?.query,
|
|
1201
1307
|
// Optional metadata
|
|
1202
|
-
analytics_tags: [
|
|
1308
|
+
analytics_tags: ["source:search", "auto_tracked:true"],
|
|
1203
1309
|
}));
|
|
1204
1310
|
try {
|
|
1205
1311
|
await this.trackEvents(impressionEvents);
|
|
1206
|
-
this.logger.debug(
|
|
1312
|
+
this.logger.debug("Auto-tracked search impressions", {
|
|
1313
|
+
count: impressionEvents.length,
|
|
1314
|
+
});
|
|
1207
1315
|
}
|
|
1208
1316
|
catch (error) {
|
|
1209
|
-
this.logger.error(
|
|
1317
|
+
this.logger.error("Failed to auto-track impressions", {
|
|
1210
1318
|
error: error.message,
|
|
1211
|
-
itemCount: items.length
|
|
1319
|
+
itemCount: items.length,
|
|
1212
1320
|
});
|
|
1213
1321
|
throw error;
|
|
1214
1322
|
}
|
|
@@ -1245,19 +1353,22 @@ class SeekoraClient {
|
|
|
1245
1353
|
// Format: userKey_eventType_itemId_timestampWindow
|
|
1246
1354
|
// Item ID is optional (not all events have items)
|
|
1247
1355
|
const parts = [
|
|
1248
|
-
userId.replace(/[^a-zA-Z0-9]/g,
|
|
1249
|
-
eventType.replace(/[^a-zA-Z0-9]/g,
|
|
1250
|
-
itemId ? itemId.replace(/[^a-zA-Z0-9]/g,
|
|
1251
|
-
timestamp.toString()
|
|
1356
|
+
userId.replace(/[^a-zA-Z0-9]/g, "_"), // Sanitize user ID
|
|
1357
|
+
eventType.replace(/[^a-zA-Z0-9]/g, "_"), // Sanitize event type
|
|
1358
|
+
itemId ? itemId.replace(/[^a-zA-Z0-9]/g, "_") : "null",
|
|
1359
|
+
timestamp.toString(),
|
|
1252
1360
|
];
|
|
1253
|
-
return parts.join(
|
|
1361
|
+
return parts.join("_");
|
|
1254
1362
|
}
|
|
1255
1363
|
/**
|
|
1256
1364
|
* Build event payload with identifiers and browser context
|
|
1257
1365
|
* Ensures user_id or anon_id is present, and sets correlation_id/search_id at top level
|
|
1258
1366
|
*/
|
|
1259
1367
|
buildEventPayload(event, context) {
|
|
1260
|
-
const anonId = event.anonymous_id ??
|
|
1368
|
+
const anonId = event.anonymous_id ??
|
|
1369
|
+
event.anon_id ??
|
|
1370
|
+
context?.anonId ??
|
|
1371
|
+
this.anonId;
|
|
1261
1372
|
const payload = {
|
|
1262
1373
|
...event,
|
|
1263
1374
|
// Set identifiers from context or fall back to client defaults
|
|
@@ -1265,14 +1376,18 @@ class SeekoraClient {
|
|
|
1265
1376
|
anon_id: anonId,
|
|
1266
1377
|
session_id: event.session_id || context?.sessionId || this.sessionId,
|
|
1267
1378
|
// Set correlation_id at top level if provided (per identifier spec)
|
|
1268
|
-
correlation_id: event.correlation_id ||
|
|
1379
|
+
correlation_id: event.correlation_id ||
|
|
1380
|
+
context?.correlationId,
|
|
1269
1381
|
// Set search_id at top level (not just in metadata) if provided (per identifier spec)
|
|
1270
1382
|
search_id: event.search_id || context?.searchId,
|
|
1271
1383
|
// V3 analytics: event timestamp (ISO) and anonymous_id alias for v3 backend compatibility
|
|
1272
1384
|
event_ts: event.event_ts ?? new Date().toISOString(),
|
|
1273
1385
|
anonymous_id: anonId,
|
|
1274
|
-
orgcode: event.orgcode ??
|
|
1275
|
-
|
|
1386
|
+
orgcode: event.orgcode ??
|
|
1387
|
+
context?.orgcode,
|
|
1388
|
+
xstoreid: event.xstoreid ??
|
|
1389
|
+
context?.xstoreId ??
|
|
1390
|
+
this.storeId,
|
|
1276
1391
|
};
|
|
1277
1392
|
// Ensure either user_id or anon_id is present
|
|
1278
1393
|
if (!payload.user_id && !payload.anon_id) {
|
|
@@ -1343,21 +1458,22 @@ class SeekoraClient {
|
|
|
1343
1458
|
// Generate insert_id for deduplication (industry standard: Segment/Amplitude pattern)
|
|
1344
1459
|
// Always enable for purchase events (revenue must be accurate)
|
|
1345
1460
|
// Optional for other events based on config
|
|
1346
|
-
const isPurchaseEvent = payload.event_name ===
|
|
1347
|
-
(payload.conversion_type ===
|
|
1461
|
+
const isPurchaseEvent = payload.event_name === "conversion" &&
|
|
1462
|
+
(payload.conversion_type === "purchase" ||
|
|
1463
|
+
payload.conversion_type === "checkout_completed");
|
|
1348
1464
|
if (this.enableDeduplication || isPurchaseEvent) {
|
|
1349
1465
|
const userKey = payload.user_id || payload.anon_id || this.anonId;
|
|
1350
|
-
const eventType = payload.event_name ||
|
|
1466
|
+
const eventType = payload.event_name || "unknown";
|
|
1351
1467
|
const itemId = payload.clicked_item_id;
|
|
1352
1468
|
// Use longer window for purchase events (30 days)
|
|
1353
1469
|
const window = isPurchaseEvent ? 2592000000 : this.deduplicationWindow;
|
|
1354
1470
|
payload.insert_id = this.generateInsertId(eventType, itemId, userKey, window);
|
|
1355
|
-
this.logger.verbose(
|
|
1471
|
+
this.logger.verbose("Generated insert_id for deduplication", {
|
|
1356
1472
|
event_name: eventType,
|
|
1357
1473
|
item_id: itemId,
|
|
1358
1474
|
insert_id: payload.insert_id,
|
|
1359
1475
|
is_purchase: isPurchaseEvent,
|
|
1360
|
-
window_ms: window
|
|
1476
|
+
window_ms: window,
|
|
1361
1477
|
});
|
|
1362
1478
|
}
|
|
1363
1479
|
return payload;
|
|
@@ -1369,41 +1485,44 @@ class SeekoraClient {
|
|
|
1369
1485
|
* @param context - Optional search context for linking events to searches
|
|
1370
1486
|
*/
|
|
1371
1487
|
async trackEvent(event, context) {
|
|
1372
|
-
this.logger.verbose(
|
|
1488
|
+
this.logger.verbose("Tracking analytics event", {
|
|
1373
1489
|
eventName: event.event_name,
|
|
1374
|
-
hasContext: !!context
|
|
1490
|
+
hasContext: !!context,
|
|
1375
1491
|
});
|
|
1376
1492
|
const payload = this.buildEventPayload(event, context);
|
|
1377
1493
|
// Use event queue if enabled
|
|
1378
1494
|
if (this.enableEventQueue && this.eventQueue) {
|
|
1379
1495
|
this.eventQueue.enqueue(payload);
|
|
1380
|
-
this.logger.verbose(
|
|
1496
|
+
this.logger.verbose("Event queued for sending", {
|
|
1381
1497
|
eventName: event.event_name,
|
|
1382
|
-
queueSize: this.eventQueue.size()
|
|
1498
|
+
queueSize: this.eventQueue.size(),
|
|
1383
1499
|
});
|
|
1384
1500
|
return;
|
|
1385
1501
|
}
|
|
1386
1502
|
try {
|
|
1387
|
-
this.logger.verbose(
|
|
1388
|
-
endpoint:
|
|
1389
|
-
eventName: event.event_name
|
|
1503
|
+
this.logger.verbose("Sending analytics event", {
|
|
1504
|
+
endpoint: "/api/analytics/event",
|
|
1505
|
+
eventName: event.event_name,
|
|
1390
1506
|
});
|
|
1391
1507
|
// Cast to DataTypesEventPayload for API call (backend accepts extended fields)
|
|
1392
1508
|
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
|
|
1393
1509
|
{
|
|
1394
1510
|
headers: {
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
}
|
|
1511
|
+
"x-storeid": this.storeId,
|
|
1512
|
+
"x-storesecret": this.readSecret,
|
|
1513
|
+
},
|
|
1398
1514
|
});
|
|
1399
|
-
this.logger.verbose(
|
|
1515
|
+
this.logger.verbose("Analytics event tracked successfully", {
|
|
1400
1516
|
eventName: event.event_name,
|
|
1401
|
-
status: response.status
|
|
1517
|
+
status: response.status,
|
|
1402
1518
|
});
|
|
1403
1519
|
}
|
|
1404
1520
|
catch (error) {
|
|
1405
|
-
this.logger.error(
|
|
1406
|
-
|
|
1521
|
+
this.logger.error("Failed to track event", {
|
|
1522
|
+
eventName: event.event_name,
|
|
1523
|
+
error: error.message,
|
|
1524
|
+
});
|
|
1525
|
+
throw this.handleError(error, "trackEvent");
|
|
1407
1526
|
}
|
|
1408
1527
|
}
|
|
1409
1528
|
/**
|
|
@@ -1415,67 +1534,113 @@ class SeekoraClient {
|
|
|
1415
1534
|
* @param events - Array of event payloads (should have identifiers already set)
|
|
1416
1535
|
*/
|
|
1417
1536
|
async trackEvents(events, context) {
|
|
1418
|
-
this.logger.verbose(
|
|
1537
|
+
this.logger.verbose("Tracking batch analytics events", {
|
|
1419
1538
|
count: events.length,
|
|
1420
|
-
hasContext: !!context
|
|
1539
|
+
hasContext: !!context,
|
|
1421
1540
|
});
|
|
1422
1541
|
if (events.length > 100) {
|
|
1423
|
-
this.logger.error(
|
|
1424
|
-
|
|
1542
|
+
this.logger.error("Batch size exceeds maximum", {
|
|
1543
|
+
count: events.length,
|
|
1544
|
+
max: 100,
|
|
1545
|
+
});
|
|
1546
|
+
throw new Error("Maximum 100 events per batch");
|
|
1425
1547
|
}
|
|
1426
1548
|
// Build full event payloads with identifiers
|
|
1427
|
-
const enrichedEvents = events.map(event => this.buildEventPayload(event, context));
|
|
1549
|
+
const enrichedEvents = events.map((event) => this.buildEventPayload(event, context));
|
|
1428
1550
|
// Use event queue if enabled
|
|
1429
1551
|
if (this.enableEventQueue && this.eventQueue) {
|
|
1430
1552
|
this.eventQueue.enqueueBatch(enrichedEvents);
|
|
1431
|
-
this.logger.verbose(
|
|
1553
|
+
this.logger.verbose("Events queued for sending", {
|
|
1432
1554
|
count: events.length,
|
|
1433
|
-
queueSize: this.eventQueue.size()
|
|
1555
|
+
queueSize: this.eventQueue.size(),
|
|
1434
1556
|
});
|
|
1435
1557
|
return;
|
|
1436
1558
|
}
|
|
1437
1559
|
try {
|
|
1438
|
-
this.logger.verbose(
|
|
1439
|
-
endpoint:
|
|
1440
|
-
eventCount: events.length
|
|
1560
|
+
this.logger.verbose("Sending batch analytics events", {
|
|
1561
|
+
endpoint: "/api/analytics/batch",
|
|
1562
|
+
eventCount: events.length,
|
|
1441
1563
|
});
|
|
1442
1564
|
const batchRequest = {
|
|
1443
|
-
events: enrichedEvents // Type assertion for extended payloads
|
|
1565
|
+
events: enrichedEvents, // Type assertion for extended payloads
|
|
1444
1566
|
};
|
|
1445
1567
|
const response = await this.analyticsApi.analyticsBatchPost(this.storeId, this.readSecret, batchRequest, {
|
|
1446
1568
|
headers: {
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
}
|
|
1569
|
+
"x-storeid": this.storeId,
|
|
1570
|
+
"x-storesecret": this.readSecret,
|
|
1571
|
+
},
|
|
1450
1572
|
});
|
|
1451
|
-
this.logger.info(
|
|
1573
|
+
this.logger.info("Batch analytics events tracked successfully", {
|
|
1452
1574
|
count: events.length,
|
|
1453
|
-
status: response.status
|
|
1575
|
+
status: response.status,
|
|
1454
1576
|
});
|
|
1455
1577
|
}
|
|
1456
1578
|
catch (error) {
|
|
1457
|
-
this.logger.error(
|
|
1458
|
-
|
|
1579
|
+
this.logger.error("Failed to track batch events", {
|
|
1580
|
+
count: events.length,
|
|
1581
|
+
error: error.message,
|
|
1582
|
+
});
|
|
1583
|
+
throw this.handleError(error, "trackEvents");
|
|
1459
1584
|
}
|
|
1460
1585
|
}
|
|
1461
1586
|
/**
|
|
1462
|
-
* Track a search event
|
|
1587
|
+
* Track a search event (Segment "Products Searched" spec)
|
|
1463
1588
|
*
|
|
1464
1589
|
* @param params - Search tracking parameters
|
|
1465
1590
|
*/
|
|
1466
1591
|
async trackSearch(params) {
|
|
1467
|
-
this.
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
});
|
|
1472
|
-
await this.trackEvent({
|
|
1473
|
-
event_name: 'search.performed',
|
|
1592
|
+
const analytics = this.getAnalytics();
|
|
1593
|
+
if (!analytics)
|
|
1594
|
+
return;
|
|
1595
|
+
const properties = {
|
|
1474
1596
|
query: params.query,
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1597
|
+
num_found: params.numFound ?? params.resultsCount ?? 0,
|
|
1598
|
+
search_id: params.searchId,
|
|
1599
|
+
latency_ms: params.latencyMs,
|
|
1600
|
+
engine: params.engine,
|
|
1601
|
+
results: (params.results || []).map((r, i) => ({
|
|
1602
|
+
product_id: r.id,
|
|
1603
|
+
sku: r.sku,
|
|
1604
|
+
category: r.category,
|
|
1605
|
+
position: i + 1,
|
|
1606
|
+
})),
|
|
1607
|
+
};
|
|
1608
|
+
await analytics.track("Products Searched", properties);
|
|
1609
|
+
}
|
|
1610
|
+
/**
|
|
1611
|
+
* Track a product click event (Segment "Product Clicked" spec)
|
|
1612
|
+
*
|
|
1613
|
+
* @param params - Product click parameters
|
|
1614
|
+
*/
|
|
1615
|
+
async trackProductClick(params) {
|
|
1616
|
+
const analytics = this.getAnalytics();
|
|
1617
|
+
if (!analytics)
|
|
1618
|
+
return;
|
|
1619
|
+
const properties = {
|
|
1620
|
+
product_id: params.productId,
|
|
1621
|
+
sku: params.sku,
|
|
1622
|
+
position: params.position,
|
|
1623
|
+
search_id: params.searchId,
|
|
1624
|
+
};
|
|
1625
|
+
await analytics.track("Product Clicked", properties);
|
|
1626
|
+
}
|
|
1627
|
+
/**
|
|
1628
|
+
* Track a product impression event (Segment "Product Viewed" spec)
|
|
1629
|
+
*
|
|
1630
|
+
* @param params - Product impression parameters
|
|
1631
|
+
*/
|
|
1632
|
+
async trackProductImpression(params) {
|
|
1633
|
+
const analytics = this.getAnalytics();
|
|
1634
|
+
if (!analytics)
|
|
1635
|
+
return;
|
|
1636
|
+
const properties = {
|
|
1637
|
+
product_id: params.productId,
|
|
1638
|
+
sku: params.sku,
|
|
1639
|
+
position: params.position,
|
|
1640
|
+
search_id: params.searchId,
|
|
1641
|
+
category: params.category,
|
|
1642
|
+
};
|
|
1643
|
+
await analytics.track("Product Viewed", properties);
|
|
1479
1644
|
}
|
|
1480
1645
|
/**
|
|
1481
1646
|
* Track an impression event
|
|
@@ -1483,13 +1648,13 @@ class SeekoraClient {
|
|
|
1483
1648
|
* @param params - Impression tracking parameters
|
|
1484
1649
|
*/
|
|
1485
1650
|
async trackImpression(params) {
|
|
1486
|
-
this.logger.verbose(
|
|
1651
|
+
this.logger.verbose("Tracking impression event", {
|
|
1487
1652
|
itemId: params.itemId,
|
|
1488
1653
|
position: params.position,
|
|
1489
|
-
hasContext: !!params.context
|
|
1654
|
+
hasContext: !!params.context,
|
|
1490
1655
|
});
|
|
1491
1656
|
await this.trackEvent({
|
|
1492
|
-
event_name:
|
|
1657
|
+
event_name: "impression",
|
|
1493
1658
|
clicked_item_id: params.itemId,
|
|
1494
1659
|
metadata: {
|
|
1495
1660
|
position: params.position,
|
|
@@ -1500,17 +1665,22 @@ class SeekoraClient {
|
|
|
1500
1665
|
}, params.context);
|
|
1501
1666
|
}
|
|
1502
1667
|
async trackClick(itemId, position, contextOrSearchId) {
|
|
1503
|
-
this.logger.verbose(
|
|
1668
|
+
this.logger.verbose("Tracking click event", {
|
|
1504
1669
|
itemId,
|
|
1505
1670
|
position,
|
|
1506
|
-
hasContext: !!contextOrSearchId
|
|
1671
|
+
hasContext: !!contextOrSearchId,
|
|
1507
1672
|
});
|
|
1508
1673
|
// Handle backward compatibility: if third param is string, treat as searchId
|
|
1509
|
-
const context = typeof contextOrSearchId ===
|
|
1510
|
-
? {
|
|
1674
|
+
const context = typeof contextOrSearchId === "string"
|
|
1675
|
+
? {
|
|
1676
|
+
correlationId: "",
|
|
1677
|
+
searchId: contextOrSearchId,
|
|
1678
|
+
anonId: this.anonId,
|
|
1679
|
+
sessionId: this.sessionId,
|
|
1680
|
+
}
|
|
1511
1681
|
: contextOrSearchId;
|
|
1512
1682
|
await this.trackEvent({
|
|
1513
|
-
event_name:
|
|
1683
|
+
event_name: "product_click",
|
|
1514
1684
|
clicked_item_id: itemId,
|
|
1515
1685
|
position,
|
|
1516
1686
|
metadata: {
|
|
@@ -1519,32 +1689,42 @@ class SeekoraClient {
|
|
|
1519
1689
|
}, context);
|
|
1520
1690
|
}
|
|
1521
1691
|
async trackView(itemId, contextOrSearchId) {
|
|
1522
|
-
this.logger.verbose(
|
|
1692
|
+
this.logger.verbose("Tracking view event", {
|
|
1523
1693
|
itemId,
|
|
1524
|
-
hasContext: !!contextOrSearchId
|
|
1694
|
+
hasContext: !!contextOrSearchId,
|
|
1525
1695
|
});
|
|
1526
1696
|
// Handle backward compatibility: if second param is string, treat as searchId
|
|
1527
|
-
const context = typeof contextOrSearchId ===
|
|
1528
|
-
? {
|
|
1697
|
+
const context = typeof contextOrSearchId === "string"
|
|
1698
|
+
? {
|
|
1699
|
+
correlationId: "",
|
|
1700
|
+
searchId: contextOrSearchId,
|
|
1701
|
+
anonId: this.anonId,
|
|
1702
|
+
sessionId: this.sessionId,
|
|
1703
|
+
}
|
|
1529
1704
|
: contextOrSearchId;
|
|
1530
1705
|
await this.trackEvent({
|
|
1531
|
-
event_name:
|
|
1706
|
+
event_name: "view",
|
|
1532
1707
|
clicked_item_id: itemId,
|
|
1533
1708
|
}, context);
|
|
1534
1709
|
}
|
|
1535
1710
|
async trackConversion(itemId, value, currency, contextOrSearchId) {
|
|
1536
|
-
this.logger.verbose(
|
|
1711
|
+
this.logger.verbose("Tracking conversion event", {
|
|
1537
1712
|
itemId,
|
|
1538
1713
|
value,
|
|
1539
1714
|
currency,
|
|
1540
|
-
hasContext: !!contextOrSearchId
|
|
1715
|
+
hasContext: !!contextOrSearchId,
|
|
1541
1716
|
});
|
|
1542
1717
|
// Handle backward compatibility: if fourth param is string, treat as searchId
|
|
1543
|
-
const context = typeof contextOrSearchId ===
|
|
1544
|
-
? {
|
|
1718
|
+
const context = typeof contextOrSearchId === "string"
|
|
1719
|
+
? {
|
|
1720
|
+
correlationId: "",
|
|
1721
|
+
searchId: contextOrSearchId,
|
|
1722
|
+
anonId: this.anonId,
|
|
1723
|
+
sessionId: this.sessionId,
|
|
1724
|
+
}
|
|
1545
1725
|
: contextOrSearchId;
|
|
1546
1726
|
await this.trackEvent({
|
|
1547
|
-
event_name:
|
|
1727
|
+
event_name: "conversion",
|
|
1548
1728
|
clicked_item_id: itemId,
|
|
1549
1729
|
value,
|
|
1550
1730
|
currency,
|
|
@@ -1558,9 +1738,9 @@ class SeekoraClient {
|
|
|
1558
1738
|
* @param context - Optional search context for linking events to searches
|
|
1559
1739
|
*/
|
|
1560
1740
|
async trackCustom(eventName, payload = {}, context) {
|
|
1561
|
-
this.logger.verbose(
|
|
1741
|
+
this.logger.verbose("Tracking custom event", {
|
|
1562
1742
|
eventName,
|
|
1563
|
-
hasContext: !!context
|
|
1743
|
+
hasContext: !!context,
|
|
1564
1744
|
});
|
|
1565
1745
|
await this.trackEvent({
|
|
1566
1746
|
...payload,
|
|
@@ -1579,20 +1759,20 @@ class SeekoraClient {
|
|
|
1579
1759
|
* @param params.searchContext - Search context for attribution
|
|
1580
1760
|
*/
|
|
1581
1761
|
async trackAddToCart(params) {
|
|
1582
|
-
this.logger.verbose(
|
|
1762
|
+
this.logger.verbose("Tracking add to cart event", {
|
|
1583
1763
|
itemId: params.itemId,
|
|
1584
1764
|
quantity: params.quantity,
|
|
1585
|
-
value: params.value
|
|
1765
|
+
value: params.value,
|
|
1586
1766
|
});
|
|
1587
1767
|
await this.trackEvent({
|
|
1588
|
-
event_name:
|
|
1589
|
-
conversion_type:
|
|
1768
|
+
event_name: "conversion",
|
|
1769
|
+
conversion_type: "add_to_cart",
|
|
1590
1770
|
clicked_item_id: params.itemId,
|
|
1591
1771
|
quantity: params.quantity || 1,
|
|
1592
1772
|
value: params.value,
|
|
1593
|
-
currency: params.currency ||
|
|
1773
|
+
currency: params.currency || "USD",
|
|
1594
1774
|
position: params.position,
|
|
1595
|
-
analytics_tags: [
|
|
1775
|
+
analytics_tags: ["action:add_to_cart"],
|
|
1596
1776
|
}, params.searchContext);
|
|
1597
1777
|
}
|
|
1598
1778
|
/**
|
|
@@ -1604,15 +1784,15 @@ class SeekoraClient {
|
|
|
1604
1784
|
* @param params.searchContext - Search context for attribution
|
|
1605
1785
|
*/
|
|
1606
1786
|
async trackAddToWishlist(params) {
|
|
1607
|
-
this.logger.verbose(
|
|
1608
|
-
itemId: params.itemId
|
|
1787
|
+
this.logger.verbose("Tracking add to wishlist event", {
|
|
1788
|
+
itemId: params.itemId,
|
|
1609
1789
|
});
|
|
1610
1790
|
await this.trackEvent({
|
|
1611
|
-
event_name:
|
|
1612
|
-
conversion_type:
|
|
1791
|
+
event_name: "conversion",
|
|
1792
|
+
conversion_type: "wishlist",
|
|
1613
1793
|
clicked_item_id: params.itemId,
|
|
1614
1794
|
position: params.position,
|
|
1615
|
-
analytics_tags: [
|
|
1795
|
+
analytics_tags: ["action:add_to_wishlist"],
|
|
1616
1796
|
}, params.searchContext);
|
|
1617
1797
|
}
|
|
1618
1798
|
/**
|
|
@@ -1625,21 +1805,21 @@ class SeekoraClient {
|
|
|
1625
1805
|
* @param params.searchContext - Search context for attribution
|
|
1626
1806
|
*/
|
|
1627
1807
|
async trackShare(params) {
|
|
1628
|
-
this.logger.verbose(
|
|
1808
|
+
this.logger.verbose("Tracking share event", {
|
|
1629
1809
|
itemId: params.itemId,
|
|
1630
|
-
shareMethod: params.shareMethod
|
|
1810
|
+
shareMethod: params.shareMethod,
|
|
1631
1811
|
});
|
|
1632
1812
|
await this.trackEvent({
|
|
1633
|
-
event_name:
|
|
1813
|
+
event_name: "custom",
|
|
1634
1814
|
clicked_item_id: params.itemId,
|
|
1635
1815
|
position: params.position,
|
|
1636
1816
|
analytics_tags: [
|
|
1637
|
-
|
|
1638
|
-
`share_method:${params.shareMethod ||
|
|
1817
|
+
"action:share",
|
|
1818
|
+
`share_method:${params.shareMethod || "unknown"}`,
|
|
1639
1819
|
],
|
|
1640
1820
|
custom_json: JSON.stringify({
|
|
1641
1821
|
share_method: params.shareMethod,
|
|
1642
|
-
action_type:
|
|
1822
|
+
action_type: "share",
|
|
1643
1823
|
}),
|
|
1644
1824
|
}, params.searchContext);
|
|
1645
1825
|
}
|
|
@@ -1651,37 +1831,42 @@ class SeekoraClient {
|
|
|
1651
1831
|
* @returns Validation result with success status and any error messages
|
|
1652
1832
|
*/
|
|
1653
1833
|
async validateEvent(event) {
|
|
1654
|
-
this.logger.verbose(
|
|
1834
|
+
this.logger.verbose("Validating event payload", {
|
|
1835
|
+
eventName: event.event_name,
|
|
1836
|
+
});
|
|
1655
1837
|
try {
|
|
1656
|
-
this.logger.verbose(
|
|
1657
|
-
endpoint:
|
|
1658
|
-
eventName: event.event_name
|
|
1838
|
+
this.logger.verbose("Validating event payload", {
|
|
1839
|
+
endpoint: "/api/analytics/validate",
|
|
1840
|
+
eventName: event.event_name,
|
|
1659
1841
|
});
|
|
1660
1842
|
const payload = this.buildEventPayload(event);
|
|
1661
1843
|
const response = await this.analyticsApi.analyticsValidatePost(this.storeId, this.readSecret, payload, {
|
|
1662
1844
|
headers: {
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
}
|
|
1845
|
+
"x-storeid": this.storeId,
|
|
1846
|
+
"x-storesecret": this.readSecret,
|
|
1847
|
+
},
|
|
1666
1848
|
});
|
|
1667
1849
|
const result = response.data?.data || response.data;
|
|
1668
1850
|
const isValid = result?.valid !== false && response.status === 200;
|
|
1669
|
-
this.logger.verbose(
|
|
1851
|
+
this.logger.verbose("Event validation result", {
|
|
1670
1852
|
valid: isValid,
|
|
1671
1853
|
eventName: event.event_name,
|
|
1672
|
-
message: result?.message
|
|
1854
|
+
message: result?.message,
|
|
1673
1855
|
});
|
|
1674
1856
|
return {
|
|
1675
1857
|
valid: isValid,
|
|
1676
|
-
message: result?.message ||
|
|
1858
|
+
message: result?.message ||
|
|
1859
|
+
(isValid ? "Event is valid" : "Event validation failed"),
|
|
1677
1860
|
errors: result?.errors,
|
|
1678
1861
|
};
|
|
1679
1862
|
}
|
|
1680
1863
|
catch (error) {
|
|
1681
|
-
this.logger.error(
|
|
1864
|
+
this.logger.error("Event validation failed", { error: error.message });
|
|
1682
1865
|
return {
|
|
1683
1866
|
valid: false,
|
|
1684
|
-
message: error.response?.data?.message ||
|
|
1867
|
+
message: error.response?.data?.message ||
|
|
1868
|
+
error.message ||
|
|
1869
|
+
"Validation request failed",
|
|
1685
1870
|
errors: error.response?.data?.errors || error.response?.data,
|
|
1686
1871
|
};
|
|
1687
1872
|
}
|
|
@@ -1693,21 +1878,23 @@ class SeekoraClient {
|
|
|
1693
1878
|
* @returns Event schema with field definitions, types, and validation rules
|
|
1694
1879
|
*/
|
|
1695
1880
|
async getEventSchema() {
|
|
1696
|
-
this.logger.verbose(
|
|
1881
|
+
this.logger.verbose("Getting event schema");
|
|
1697
1882
|
try {
|
|
1698
1883
|
const response = await this.analyticsApi.analyticsSchemaGet(this.storeId, this.readSecret, {
|
|
1699
1884
|
headers: {
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
}
|
|
1885
|
+
"x-storeid": this.storeId,
|
|
1886
|
+
"x-storesecret": this.readSecret,
|
|
1887
|
+
},
|
|
1703
1888
|
});
|
|
1704
1889
|
const schema = response.data?.data || response.data;
|
|
1705
|
-
this.logger.verbose(
|
|
1890
|
+
this.logger.verbose("Event schema retrieved", {
|
|
1891
|
+
schemaKeys: schema ? Object.keys(schema) : [],
|
|
1892
|
+
});
|
|
1706
1893
|
return schema;
|
|
1707
1894
|
}
|
|
1708
1895
|
catch (error) {
|
|
1709
|
-
this.logger.error(
|
|
1710
|
-
throw this.handleError(error,
|
|
1896
|
+
this.logger.error("Failed to get event schema", { error: error.message });
|
|
1897
|
+
throw this.handleError(error, "getEventSchema");
|
|
1711
1898
|
}
|
|
1712
1899
|
}
|
|
1713
1900
|
/**
|
|
@@ -1717,14 +1904,14 @@ class SeekoraClient {
|
|
|
1717
1904
|
*/
|
|
1718
1905
|
setUserId(userId) {
|
|
1719
1906
|
this.userId = userId;
|
|
1720
|
-
this.logger.info(
|
|
1907
|
+
this.logger.info("User ID updated", { userId });
|
|
1721
1908
|
}
|
|
1722
1909
|
/**
|
|
1723
1910
|
* Clear user ID (call this when user logs out)
|
|
1724
1911
|
*/
|
|
1725
1912
|
clearUserId() {
|
|
1726
1913
|
this.userId = undefined;
|
|
1727
|
-
this.logger.info(
|
|
1914
|
+
this.logger.info("User ID cleared");
|
|
1728
1915
|
}
|
|
1729
1916
|
/**
|
|
1730
1917
|
* Get current identifiers
|
|
@@ -1744,7 +1931,7 @@ class SeekoraClient {
|
|
|
1744
1931
|
setAbTest(abTestId, abVariant) {
|
|
1745
1932
|
this.abTestId = abTestId;
|
|
1746
1933
|
this.abVariant = abVariant;
|
|
1747
|
-
this.logger.verbose(
|
|
1934
|
+
this.logger.verbose("A/B test fields updated", { abTestId, abVariant });
|
|
1748
1935
|
}
|
|
1749
1936
|
/**
|
|
1750
1937
|
* Get current A/B test fields
|
|
@@ -1776,7 +1963,7 @@ class SeekoraClient {
|
|
|
1776
1963
|
}
|
|
1777
1964
|
return {
|
|
1778
1965
|
enabled: this.enableEventQueue,
|
|
1779
|
-
...this.eventQueue.getStats()
|
|
1966
|
+
...this.eventQueue.getStats(),
|
|
1780
1967
|
};
|
|
1781
1968
|
}
|
|
1782
1969
|
/**
|
|
@@ -1787,38 +1974,12 @@ class SeekoraClient {
|
|
|
1787
1974
|
* @param traits - Optional user traits/properties
|
|
1788
1975
|
*/
|
|
1789
1976
|
async identify(userId, traits) {
|
|
1790
|
-
this.logger.verbose(
|
|
1791
|
-
// Update internal user ID
|
|
1977
|
+
this.logger.verbose("Identifying user", { userId });
|
|
1792
1978
|
this.userId = userId;
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
|
|
1796
|
-
|
|
1797
|
-
anon_id: this.anonId,
|
|
1798
|
-
session_id: this.sessionId,
|
|
1799
|
-
device_fingerprint: context?.device_fingerprint,
|
|
1800
|
-
traits,
|
|
1801
|
-
};
|
|
1802
|
-
try {
|
|
1803
|
-
const response = await axios_1.default.post(`${this.config.basePath}/api/analytics/identify`, payload, {
|
|
1804
|
-
headers: {
|
|
1805
|
-
'x-storeid': this.storeId,
|
|
1806
|
-
'x-storesecret': this.readSecret,
|
|
1807
|
-
},
|
|
1808
|
-
timeout: 10000,
|
|
1809
|
-
});
|
|
1810
|
-
this.logger.info('User identified successfully', {
|
|
1811
|
-
userId,
|
|
1812
|
-
linked: {
|
|
1813
|
-
anonId: !!this.anonId,
|
|
1814
|
-
fingerprint: !!context?.device_fingerprint,
|
|
1815
|
-
}
|
|
1816
|
-
});
|
|
1817
|
-
}
|
|
1818
|
-
catch (error) {
|
|
1819
|
-
this.logger.error('Failed to identify user', { userId, error: error.message });
|
|
1820
|
-
// Don't throw - identify failures shouldn't break the app
|
|
1821
|
-
}
|
|
1979
|
+
const analytics = this.getAnalytics();
|
|
1980
|
+
if (!analytics)
|
|
1981
|
+
return;
|
|
1982
|
+
await analytics.identify(userId, traits);
|
|
1822
1983
|
}
|
|
1823
1984
|
/**
|
|
1824
1985
|
* Alias an anonymous ID to a user ID
|
|
@@ -1828,7 +1989,7 @@ class SeekoraClient {
|
|
|
1828
1989
|
* @param userId - The user ID to link to
|
|
1829
1990
|
*/
|
|
1830
1991
|
async alias(anonId, userId) {
|
|
1831
|
-
this.logger.verbose(
|
|
1992
|
+
this.logger.verbose("Creating alias", { anonId, userId });
|
|
1832
1993
|
const payload = {
|
|
1833
1994
|
user_id: userId,
|
|
1834
1995
|
anon_id: anonId,
|
|
@@ -1836,15 +1997,19 @@ class SeekoraClient {
|
|
|
1836
1997
|
try {
|
|
1837
1998
|
await axios_1.default.post(`${this.config.basePath}/api/analytics/identify`, payload, {
|
|
1838
1999
|
headers: {
|
|
1839
|
-
|
|
1840
|
-
|
|
2000
|
+
"x-storeid": this.storeId,
|
|
2001
|
+
"x-storesecret": this.readSecret,
|
|
1841
2002
|
},
|
|
1842
2003
|
timeout: 10000,
|
|
1843
2004
|
});
|
|
1844
|
-
this.logger.info(
|
|
2005
|
+
this.logger.info("Alias created successfully", { anonId, userId });
|
|
1845
2006
|
}
|
|
1846
2007
|
catch (error) {
|
|
1847
|
-
this.logger.error(
|
|
2008
|
+
this.logger.error("Failed to create alias", {
|
|
2009
|
+
anonId,
|
|
2010
|
+
userId,
|
|
2011
|
+
error: error.message,
|
|
2012
|
+
});
|
|
1848
2013
|
}
|
|
1849
2014
|
}
|
|
1850
2015
|
/**
|
|
@@ -1857,25 +2022,29 @@ class SeekoraClient {
|
|
|
1857
2022
|
const message = data?.message || `API request failed with status ${status}`;
|
|
1858
2023
|
// Log based on status code
|
|
1859
2024
|
if (status >= 500) {
|
|
1860
|
-
this.logger.error(
|
|
2025
|
+
this.logger.error("API server error", { operation, status, message });
|
|
1861
2026
|
}
|
|
1862
2027
|
else if (status === 401 || status === 403) {
|
|
1863
|
-
this.logger.error(
|
|
2028
|
+
this.logger.error("Authentication error", {
|
|
2029
|
+
operation,
|
|
2030
|
+
status,
|
|
2031
|
+
message,
|
|
2032
|
+
});
|
|
1864
2033
|
}
|
|
1865
2034
|
else if (status === 429) {
|
|
1866
|
-
this.logger.warn(
|
|
2035
|
+
this.logger.warn("Rate limit exceeded", { operation, status, message });
|
|
1867
2036
|
}
|
|
1868
2037
|
else {
|
|
1869
|
-
this.logger.error(
|
|
2038
|
+
this.logger.error("API request failed", { operation, status, message });
|
|
1870
2039
|
}
|
|
1871
2040
|
return new Error(`[${operation}] ${message} (${status})`);
|
|
1872
2041
|
}
|
|
1873
2042
|
else if (error.request) {
|
|
1874
|
-
this.logger.error(
|
|
2043
|
+
this.logger.error("Network error", { operation, error: error.message });
|
|
1875
2044
|
return new Error(`[${operation}] Network error: ${error.message}`);
|
|
1876
2045
|
}
|
|
1877
2046
|
else {
|
|
1878
|
-
this.logger.error(
|
|
2047
|
+
this.logger.error("Request error", { operation, error: error.message });
|
|
1879
2048
|
return new Error(`[${operation}] ${error.message}`);
|
|
1880
2049
|
}
|
|
1881
2050
|
}
|
|
@@ -1897,20 +2066,20 @@ class SeekoraClient {
|
|
|
1897
2066
|
* @returns Filter values with counts grouped by field
|
|
1898
2067
|
*/
|
|
1899
2068
|
async getFilters(options) {
|
|
1900
|
-
this.logger.verbose(
|
|
2069
|
+
this.logger.verbose("Getting filters", { options });
|
|
1901
2070
|
try {
|
|
1902
|
-
const response = await this.filtersApi.v1FiltersGet(this.storeId, this.readSecret, options?.q, options?.filter, options?.facetBy, options?.maxFacetValues, options?.disjunctiveFacets?.join(
|
|
2071
|
+
const response = await this.filtersApi.v1FiltersGet(this.storeId, this.readSecret, options?.q, options?.filter, options?.facetBy, options?.maxFacetValues, options?.disjunctiveFacets?.join(","));
|
|
1903
2072
|
const wrapper = response.data;
|
|
1904
2073
|
const data = wrapper?.data || response.data;
|
|
1905
|
-
this.logger.info(
|
|
2074
|
+
this.logger.info("Filters retrieved successfully", {
|
|
1906
2075
|
filterCount: data?.filters?.length,
|
|
1907
2076
|
totalResults: data?.total_results,
|
|
1908
2077
|
});
|
|
1909
2078
|
return data;
|
|
1910
2079
|
}
|
|
1911
2080
|
catch (error) {
|
|
1912
|
-
this.logger.error(
|
|
1913
|
-
throw this.handleError(error,
|
|
2081
|
+
this.logger.error("Failed to get filters", { error: error.message });
|
|
2082
|
+
throw this.handleError(error, "getFilters");
|
|
1914
2083
|
}
|
|
1915
2084
|
}
|
|
1916
2085
|
/**
|
|
@@ -1922,20 +2091,23 @@ class SeekoraClient {
|
|
|
1922
2091
|
* @returns Matching facet values with counts
|
|
1923
2092
|
*/
|
|
1924
2093
|
async searchFacetValues(facetName, options) {
|
|
1925
|
-
this.logger.verbose(
|
|
2094
|
+
this.logger.verbose("Searching facet values", { facetName, options });
|
|
1926
2095
|
try {
|
|
1927
2096
|
const response = await this.filtersApi.v1FiltersFacetNameValuesGet(this.storeId, this.readSecret, facetName, options.facetQuery, options?.q, options?.filter, options?.maxValues);
|
|
1928
2097
|
const wrapper = response.data;
|
|
1929
2098
|
const data = wrapper?.data || response.data;
|
|
1930
|
-
this.logger.info(
|
|
2099
|
+
this.logger.info("Facet values retrieved", {
|
|
1931
2100
|
facetName,
|
|
1932
2101
|
valueCount: data?.values?.length,
|
|
1933
2102
|
});
|
|
1934
2103
|
return data;
|
|
1935
2104
|
}
|
|
1936
2105
|
catch (error) {
|
|
1937
|
-
this.logger.error(
|
|
1938
|
-
|
|
2106
|
+
this.logger.error("Failed to search facet values", {
|
|
2107
|
+
facetName,
|
|
2108
|
+
error: error.message,
|
|
2109
|
+
});
|
|
2110
|
+
throw this.handleError(error, "searchFacetValues");
|
|
1939
2111
|
}
|
|
1940
2112
|
}
|
|
1941
2113
|
/**
|
|
@@ -1946,20 +2118,22 @@ class SeekoraClient {
|
|
|
1946
2118
|
* @returns Filter field metadata with types, default facets, and max values
|
|
1947
2119
|
*/
|
|
1948
2120
|
async getFiltersSchema() {
|
|
1949
|
-
this.logger.verbose(
|
|
2121
|
+
this.logger.verbose("Getting filters schema");
|
|
1950
2122
|
try {
|
|
1951
2123
|
const response = await this.filtersApi.v1FiltersSchemaGet(this.storeId, this.readSecret);
|
|
1952
2124
|
const wrapper = response.data;
|
|
1953
2125
|
const data = wrapper?.data || response.data;
|
|
1954
|
-
this.logger.info(
|
|
2126
|
+
this.logger.info("Filters schema retrieved", {
|
|
1955
2127
|
fieldCount: data?.fields?.length,
|
|
1956
2128
|
defaultFacets: data?.default_facets?.length,
|
|
1957
2129
|
});
|
|
1958
2130
|
return data;
|
|
1959
2131
|
}
|
|
1960
2132
|
catch (error) {
|
|
1961
|
-
this.logger.error(
|
|
1962
|
-
|
|
2133
|
+
this.logger.error("Failed to get filters schema", {
|
|
2134
|
+
error: error.message,
|
|
2135
|
+
});
|
|
2136
|
+
throw this.handleError(error, "getFiltersSchema");
|
|
1963
2137
|
}
|
|
1964
2138
|
}
|
|
1965
2139
|
}
|