@seekora-ai/search-sdk 1.0.0 → 1.0.2
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/cdn/_headers +3 -0
- package/dist/src/client.d.ts +311 -3
- package/dist/src/client.js +561 -4
- package/dist/src/context-collector.d.ts +273 -0
- package/dist/src/context-collector.js +868 -0
- package/dist/src/event-queue.d.ts +195 -0
- package/dist/src/event-queue.js +424 -0
- package/dist/src/index.d.ts +2 -0
- package/dist/src/index.js +9 -1
- package/package.json +1 -1
package/dist/src/client.js
CHANGED
|
@@ -14,6 +14,8 @@ const config_1 = require("./config");
|
|
|
14
14
|
const logger_1 = require("./logger");
|
|
15
15
|
const config_loader_1 = require("./config-loader");
|
|
16
16
|
const utils_1 = require("./utils");
|
|
17
|
+
const context_collector_1 = require("./context-collector");
|
|
18
|
+
const event_queue_1 = require("./event-queue");
|
|
17
19
|
/**
|
|
18
20
|
* Seekora SDK Client
|
|
19
21
|
*
|
|
@@ -21,6 +23,8 @@ const utils_1 = require("./utils");
|
|
|
21
23
|
*/
|
|
22
24
|
class SeekoraClient {
|
|
23
25
|
constructor(config = {}) {
|
|
26
|
+
this.cachedBrowserContext = null;
|
|
27
|
+
this.eventQueue = null;
|
|
24
28
|
// Load configuration from file, env, and code (in that order)
|
|
25
29
|
const mergedConfig = (0, config_loader_1.loadConfig)(config);
|
|
26
30
|
this.storeId = mergedConfig.storeId;
|
|
@@ -37,12 +41,28 @@ class SeekoraClient {
|
|
|
37
41
|
this.anonId = config.anonId || (0, utils_1.getOrCreateAnonId)();
|
|
38
42
|
this.sessionId = config.sessionId || (0, utils_1.getOrCreateSessionId)();
|
|
39
43
|
this.autoTrackSearch = config.autoTrackSearch || false;
|
|
44
|
+
// Initialize context collection
|
|
45
|
+
this.enableContextCollection = config.enableContextCollection !== false; // Default to true
|
|
46
|
+
this.contextCollector = new context_collector_1.ContextCollector(config.contextCollector);
|
|
47
|
+
// Pre-collect browser context if enabled
|
|
48
|
+
if (this.enableContextCollection) {
|
|
49
|
+
this.collectContextAsync();
|
|
50
|
+
}
|
|
51
|
+
// Initialize event queue if enabled
|
|
52
|
+
this.enableEventQueue = config.enableEventQueue || false;
|
|
53
|
+
if (this.enableEventQueue) {
|
|
54
|
+
this.eventQueue = new event_queue_1.EventQueue(config.eventQueue);
|
|
55
|
+
this.eventQueue.setLogger(this.logger);
|
|
56
|
+
this.eventQueue.setSender(this.createQueueSender());
|
|
57
|
+
}
|
|
40
58
|
// Log identifier initialization
|
|
41
59
|
this.logger.verbose('Client identifiers initialized', {
|
|
42
60
|
hasUserId: !!this.userId,
|
|
43
61
|
anonId: this.anonId.substring(0, 8) + '...', // Log partial ID for privacy
|
|
44
62
|
sessionId: this.sessionId.substring(0, 8) + '...',
|
|
45
|
-
autoTrackSearch: this.autoTrackSearch
|
|
63
|
+
autoTrackSearch: this.autoTrackSearch,
|
|
64
|
+
enableContextCollection: this.enableContextCollection,
|
|
65
|
+
enableEventQueue: this.enableEventQueue
|
|
46
66
|
});
|
|
47
67
|
this.logger.verbose('Initializing SeekoraClient', {
|
|
48
68
|
storeId: this.storeId,
|
|
@@ -69,6 +89,8 @@ class SeekoraClient {
|
|
|
69
89
|
this.suggestionsConfigApi = new generated_1.SDKQuerySuggestionsConfigApi(this.config);
|
|
70
90
|
this.analyticsApi = new generated_1.AnalyticsEventsApi(this.config);
|
|
71
91
|
this.storesApi = new generated_1.SDKStoreConfigApi(this.config);
|
|
92
|
+
this.documentsApi = new generated_1.SDKDocumentsApi(this.config);
|
|
93
|
+
this.schemaApi = new generated_1.SDKSchemaApi(this.config);
|
|
72
94
|
this.logger.info('SeekoraClient initialized successfully');
|
|
73
95
|
}
|
|
74
96
|
/**
|
|
@@ -637,8 +659,383 @@ class SeekoraClient {
|
|
|
637
659
|
throw this.handleError(error, 'getConfigSchema');
|
|
638
660
|
}
|
|
639
661
|
}
|
|
662
|
+
// ==========================================
|
|
663
|
+
// Document Indexing Methods
|
|
664
|
+
// ==========================================
|
|
665
|
+
/**
|
|
666
|
+
* Index a single document into the store
|
|
667
|
+
*
|
|
668
|
+
* @param document - Document data with optional id
|
|
669
|
+
* @returns IndexDocumentResponse with document id and status
|
|
670
|
+
*/
|
|
671
|
+
async indexDocument(document) {
|
|
672
|
+
this.logger.verbose('Indexing document', {
|
|
673
|
+
hasCustomId: !!document.id,
|
|
674
|
+
documentKeys: Object.keys(document.data)
|
|
675
|
+
});
|
|
676
|
+
if (!this.writeSecret) {
|
|
677
|
+
this.logger.error('Write secret required but not provided', {
|
|
678
|
+
operation: 'indexDocument',
|
|
679
|
+
storeId: this.storeId
|
|
680
|
+
});
|
|
681
|
+
throw new Error('Write secret is required for indexDocument');
|
|
682
|
+
}
|
|
683
|
+
try {
|
|
684
|
+
const request = {
|
|
685
|
+
id: document.id,
|
|
686
|
+
data: document.data,
|
|
687
|
+
};
|
|
688
|
+
const response = await this.documentsApi.apiV1StoresXStoreIDDocumentsPost(this.storeId, this.writeSecret, this.storeId, request);
|
|
689
|
+
const responseData = response.data?.data;
|
|
690
|
+
this.logger.info('Document indexed successfully', {
|
|
691
|
+
id: responseData?.id || document.id,
|
|
692
|
+
status: response.status
|
|
693
|
+
});
|
|
694
|
+
return {
|
|
695
|
+
id: responseData?.id || document.id || '',
|
|
696
|
+
success: responseData?.status === 'success' || responseData?.status === 'inserted' || responseData?.status === 'updated' || true,
|
|
697
|
+
message: responseData?.message,
|
|
698
|
+
data: responseData,
|
|
699
|
+
};
|
|
700
|
+
}
|
|
701
|
+
catch (error) {
|
|
702
|
+
this.logger.error('Failed to index document', { error: error.message });
|
|
703
|
+
throw this.handleError(error, 'indexDocument');
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
/**
|
|
707
|
+
* Index multiple documents in bulk
|
|
708
|
+
*
|
|
709
|
+
* @param documents - Array of documents with optional actions (insert/update/upsert/delete)
|
|
710
|
+
* @returns BulkIndexResponse with success/error counts
|
|
711
|
+
*/
|
|
712
|
+
async indexDocuments(documents) {
|
|
713
|
+
this.logger.verbose('Bulk indexing documents', {
|
|
714
|
+
count: documents.length,
|
|
715
|
+
actions: documents.map(d => d.action || 'upsert')
|
|
716
|
+
});
|
|
717
|
+
if (!this.writeSecret) {
|
|
718
|
+
this.logger.error('Write secret required but not provided', {
|
|
719
|
+
operation: 'indexDocuments',
|
|
720
|
+
storeId: this.storeId
|
|
721
|
+
});
|
|
722
|
+
throw new Error('Write secret is required for indexDocuments');
|
|
723
|
+
}
|
|
724
|
+
try {
|
|
725
|
+
const request = {
|
|
726
|
+
documents: documents.map(doc => ({
|
|
727
|
+
id: doc.id,
|
|
728
|
+
action: doc.action,
|
|
729
|
+
data: doc.data,
|
|
730
|
+
})),
|
|
731
|
+
};
|
|
732
|
+
const response = await this.documentsApi.apiV1StoresXStoreIDDocumentsBulkPost(this.storeId, this.writeSecret, this.storeId, request);
|
|
733
|
+
const responseData = response.data?.data;
|
|
734
|
+
this.logger.info('Bulk document indexing completed', {
|
|
735
|
+
successCount: responseData?.successCount || 0,
|
|
736
|
+
errorCount: responseData?.errorCount || 0,
|
|
737
|
+
status: response.status
|
|
738
|
+
});
|
|
739
|
+
return {
|
|
740
|
+
success_count: responseData?.successCount || documents.length,
|
|
741
|
+
error_count: responseData?.errorCount || 0,
|
|
742
|
+
results: responseData?.results?.map(r => ({
|
|
743
|
+
id: r.id,
|
|
744
|
+
success: r.status === 'success' || r.status === 'inserted' || r.status === 'updated' || !r.error,
|
|
745
|
+
error: r.error,
|
|
746
|
+
})) || documents.map((doc) => ({
|
|
747
|
+
id: doc.id,
|
|
748
|
+
success: true,
|
|
749
|
+
})),
|
|
750
|
+
data: responseData,
|
|
751
|
+
};
|
|
752
|
+
}
|
|
753
|
+
catch (error) {
|
|
754
|
+
this.logger.error('Failed to bulk index documents', {
|
|
755
|
+
count: documents.length,
|
|
756
|
+
error: error.message
|
|
757
|
+
});
|
|
758
|
+
throw this.handleError(error, 'indexDocuments');
|
|
759
|
+
}
|
|
760
|
+
}
|
|
761
|
+
/**
|
|
762
|
+
* Delete a document by ID
|
|
763
|
+
*
|
|
764
|
+
* @param documentId - Document ID to delete
|
|
765
|
+
*/
|
|
766
|
+
async deleteDocument(documentId) {
|
|
767
|
+
this.logger.verbose('Deleting document', { documentId });
|
|
768
|
+
if (!this.writeSecret) {
|
|
769
|
+
this.logger.error('Write secret required but not provided', {
|
|
770
|
+
operation: 'deleteDocument',
|
|
771
|
+
storeId: this.storeId
|
|
772
|
+
});
|
|
773
|
+
throw new Error('Write secret is required for deleteDocument');
|
|
774
|
+
}
|
|
775
|
+
try {
|
|
776
|
+
const response = await this.documentsApi.apiV1StoresXStoreIDDocumentsDocumentIDDelete(this.storeId, this.writeSecret, this.storeId, documentId);
|
|
777
|
+
this.logger.info('Document deleted successfully', {
|
|
778
|
+
documentId,
|
|
779
|
+
status: response.status
|
|
780
|
+
});
|
|
781
|
+
}
|
|
782
|
+
catch (error) {
|
|
783
|
+
this.logger.error('Failed to delete document', {
|
|
784
|
+
documentId,
|
|
785
|
+
error: error.message
|
|
786
|
+
});
|
|
787
|
+
throw this.handleError(error, 'deleteDocument');
|
|
788
|
+
}
|
|
789
|
+
}
|
|
790
|
+
// ==================
|
|
791
|
+
// Schema Management
|
|
792
|
+
// ==================
|
|
793
|
+
/**
|
|
794
|
+
* Create or update the collection schema for this store
|
|
795
|
+
*
|
|
796
|
+
* @param request - Schema request with fields and options
|
|
797
|
+
* @returns SchemaResponse with the created/updated schema
|
|
798
|
+
*
|
|
799
|
+
* @example
|
|
800
|
+
* // Create a new schema
|
|
801
|
+
* const schema = await client.createSchema({
|
|
802
|
+
* fields: [
|
|
803
|
+
* { name: 'title', type: 'string' },
|
|
804
|
+
* { name: 'content', type: 'string' },
|
|
805
|
+
* { name: 'url', type: 'string' },
|
|
806
|
+
* { name: 'hierarchy.lvl0', type: 'string', facet: true }
|
|
807
|
+
* ]
|
|
808
|
+
* });
|
|
809
|
+
*
|
|
810
|
+
* @example
|
|
811
|
+
* // Add new fields to existing schema (additive mode)
|
|
812
|
+
* const schema = await client.createSchema({
|
|
813
|
+
* fields: [{ name: 'newField', type: 'string' }],
|
|
814
|
+
* mode: 'additive'
|
|
815
|
+
* });
|
|
816
|
+
*
|
|
817
|
+
* @example
|
|
818
|
+
* // Replace entire schema (destructive, clears all documents)
|
|
819
|
+
* const schema = await client.createSchema({
|
|
820
|
+
* fields: [...],
|
|
821
|
+
* mode: 'replace',
|
|
822
|
+
* confirmDelete: true
|
|
823
|
+
* });
|
|
824
|
+
*/
|
|
825
|
+
async createSchema(request) {
|
|
826
|
+
this.logger.verbose('Creating/updating schema', {
|
|
827
|
+
fieldCount: request.fields.length,
|
|
828
|
+
mode: request.mode || 'additive'
|
|
829
|
+
});
|
|
830
|
+
if (!this.writeSecret) {
|
|
831
|
+
this.logger.error('Write secret required but not provided', {
|
|
832
|
+
operation: 'createSchema',
|
|
833
|
+
storeId: this.storeId
|
|
834
|
+
});
|
|
835
|
+
throw new Error('Write secret is required for createSchema');
|
|
836
|
+
}
|
|
837
|
+
try {
|
|
838
|
+
const apiRequest = {
|
|
839
|
+
fields: request.fields.map(f => ({
|
|
840
|
+
name: f.name,
|
|
841
|
+
type: f.type,
|
|
842
|
+
facet: f.facet,
|
|
843
|
+
index: f.index,
|
|
844
|
+
optional: f.optional,
|
|
845
|
+
sort: f.sort,
|
|
846
|
+
infix: f.infix,
|
|
847
|
+
locale: f.locale,
|
|
848
|
+
})),
|
|
849
|
+
mode: request.mode,
|
|
850
|
+
confirm_delete: request.confirmDelete,
|
|
851
|
+
default_sorting_field: request.defaultSortingField,
|
|
852
|
+
enable_nested_fields: request.enableNestedFields,
|
|
853
|
+
};
|
|
854
|
+
const response = await this.schemaApi.apiV1StoresXStoreIDSchemaPost(this.storeId, this.writeSecret, this.storeId, apiRequest);
|
|
855
|
+
const responseData = response.data;
|
|
856
|
+
this.logger.info('Schema created/updated successfully', {
|
|
857
|
+
name: responseData.data?.name,
|
|
858
|
+
fieldCount: responseData.data?.fields?.length
|
|
859
|
+
});
|
|
860
|
+
return {
|
|
861
|
+
name: responseData.data?.name || '',
|
|
862
|
+
fields: (responseData.data?.fields || []).map(f => ({
|
|
863
|
+
name: f.name || '',
|
|
864
|
+
type: f.type || 'string',
|
|
865
|
+
facet: f.facet,
|
|
866
|
+
index: f.index,
|
|
867
|
+
optional: f.optional,
|
|
868
|
+
sort: f.sort,
|
|
869
|
+
infix: f.infix,
|
|
870
|
+
locale: f.locale,
|
|
871
|
+
})),
|
|
872
|
+
defaultSortingField: responseData.data?.default_sorting_field,
|
|
873
|
+
numDocuments: responseData.data?.num_documents || 0,
|
|
874
|
+
createdAt: responseData.data?.created_at,
|
|
875
|
+
};
|
|
876
|
+
}
|
|
877
|
+
catch (error) {
|
|
878
|
+
this.logger.error('Failed to create/update schema', { error: error.message });
|
|
879
|
+
throw this.handleError(error, 'createSchema');
|
|
880
|
+
}
|
|
881
|
+
}
|
|
882
|
+
/**
|
|
883
|
+
* Get the current collection schema for this store
|
|
884
|
+
*
|
|
885
|
+
* @returns SchemaResponse with the current schema
|
|
886
|
+
*
|
|
887
|
+
* @example
|
|
888
|
+
* const schema = await client.getSchema();
|
|
889
|
+
* console.log('Fields:', schema.fields.map(f => f.name));
|
|
890
|
+
* console.log('Documents:', schema.numDocuments);
|
|
891
|
+
*/
|
|
892
|
+
async getSchema() {
|
|
893
|
+
this.logger.verbose('Getting schema', { storeId: this.storeId });
|
|
894
|
+
try {
|
|
895
|
+
const response = await this.schemaApi.apiV1StoresXStoreIDSchemaGet(this.storeId, this.readSecret, this.storeId);
|
|
896
|
+
const responseData = response.data;
|
|
897
|
+
this.logger.info('Schema retrieved successfully', {
|
|
898
|
+
name: responseData.data?.name,
|
|
899
|
+
fieldCount: responseData.data?.fields?.length,
|
|
900
|
+
numDocuments: responseData.data?.num_documents
|
|
901
|
+
});
|
|
902
|
+
return {
|
|
903
|
+
name: responseData.data?.name || '',
|
|
904
|
+
fields: (responseData.data?.fields || []).map(f => ({
|
|
905
|
+
name: f.name || '',
|
|
906
|
+
type: f.type || 'string',
|
|
907
|
+
facet: f.facet,
|
|
908
|
+
index: f.index,
|
|
909
|
+
optional: f.optional,
|
|
910
|
+
sort: f.sort,
|
|
911
|
+
infix: f.infix,
|
|
912
|
+
locale: f.locale,
|
|
913
|
+
})),
|
|
914
|
+
defaultSortingField: responseData.data?.default_sorting_field,
|
|
915
|
+
numDocuments: responseData.data?.num_documents || 0,
|
|
916
|
+
createdAt: responseData.data?.created_at,
|
|
917
|
+
};
|
|
918
|
+
}
|
|
919
|
+
catch (error) {
|
|
920
|
+
this.logger.error('Failed to get schema', { error: error.message });
|
|
921
|
+
throw this.handleError(error, 'getSchema');
|
|
922
|
+
}
|
|
923
|
+
}
|
|
924
|
+
/**
|
|
925
|
+
* Clear all documents from the store's collection
|
|
926
|
+
*
|
|
927
|
+
* This deletes all documents but preserves the collection and its schema.
|
|
928
|
+
* Useful for re-indexing data from scratch.
|
|
929
|
+
*
|
|
930
|
+
* @returns ClearDocumentsResponse with count of deleted documents
|
|
931
|
+
*
|
|
932
|
+
* @example
|
|
933
|
+
* const result = await client.clearDocuments();
|
|
934
|
+
* console.log(`Cleared ${result.deletedCount} documents`);
|
|
935
|
+
*/
|
|
936
|
+
async clearDocuments() {
|
|
937
|
+
this.logger.verbose('Clearing all documents', { storeId: this.storeId });
|
|
938
|
+
if (!this.writeSecret) {
|
|
939
|
+
this.logger.error('Write secret required but not provided', {
|
|
940
|
+
operation: 'clearDocuments',
|
|
941
|
+
storeId: this.storeId
|
|
942
|
+
});
|
|
943
|
+
throw new Error('Write secret is required for clearDocuments');
|
|
944
|
+
}
|
|
945
|
+
try {
|
|
946
|
+
const response = await this.schemaApi.apiV1StoresXStoreIDDocumentsDelete(this.storeId, this.writeSecret, this.storeId);
|
|
947
|
+
const responseData = response.data;
|
|
948
|
+
this.logger.info('Documents cleared successfully', {
|
|
949
|
+
deletedCount: responseData.data?.deleted_count
|
|
950
|
+
});
|
|
951
|
+
return {
|
|
952
|
+
deletedCount: responseData.data?.deleted_count || 0,
|
|
953
|
+
message: responseData.data?.message || 'Documents cleared',
|
|
954
|
+
};
|
|
955
|
+
}
|
|
956
|
+
catch (error) {
|
|
957
|
+
this.logger.error('Failed to clear documents', { error: error.message });
|
|
958
|
+
throw this.handleError(error, 'clearDocuments');
|
|
959
|
+
}
|
|
960
|
+
}
|
|
961
|
+
/**
|
|
962
|
+
* Asynchronously collect browser context (called during initialization)
|
|
963
|
+
*/
|
|
964
|
+
async collectContextAsync() {
|
|
965
|
+
try {
|
|
966
|
+
this.cachedBrowserContext = await this.contextCollector.collect();
|
|
967
|
+
this.logger.verbose('Browser context collected', {
|
|
968
|
+
hasFingerprint: !!this.cachedBrowserContext?.device_fingerprint,
|
|
969
|
+
platform: this.cachedBrowserContext?.platform,
|
|
970
|
+
timezone: this.cachedBrowserContext?.timezone,
|
|
971
|
+
});
|
|
972
|
+
}
|
|
973
|
+
catch (error) {
|
|
974
|
+
this.logger.warn('Failed to collect browser context', { error: error.message });
|
|
975
|
+
}
|
|
976
|
+
}
|
|
977
|
+
/**
|
|
978
|
+
* Create the event sender function for the event queue
|
|
979
|
+
*/
|
|
980
|
+
createQueueSender() {
|
|
981
|
+
return async (events) => {
|
|
982
|
+
if (events.length === 1) {
|
|
983
|
+
// Send single event
|
|
984
|
+
await this.sendEventDirect(events[0]);
|
|
985
|
+
}
|
|
986
|
+
else {
|
|
987
|
+
// Send as batch
|
|
988
|
+
await this.sendEventsBatchDirect(events);
|
|
989
|
+
}
|
|
990
|
+
};
|
|
991
|
+
}
|
|
992
|
+
/**
|
|
993
|
+
* Send a single event directly to the backend (bypasses queue)
|
|
994
|
+
*/
|
|
995
|
+
async sendEventDirect(payload) {
|
|
996
|
+
const response = await this.analyticsApi.analyticsEventPost(this.storeId, this.readSecret, payload, {
|
|
997
|
+
headers: {
|
|
998
|
+
'x-storeid': this.storeId,
|
|
999
|
+
'x-storesecret': this.readSecret,
|
|
1000
|
+
}
|
|
1001
|
+
});
|
|
1002
|
+
if (response.status >= 400) {
|
|
1003
|
+
throw new Error(`Failed to send event: ${response.status}`);
|
|
1004
|
+
}
|
|
1005
|
+
}
|
|
640
1006
|
/**
|
|
641
|
-
*
|
|
1007
|
+
* Send a batch of events directly to the backend (bypasses queue)
|
|
1008
|
+
*/
|
|
1009
|
+
async sendEventsBatchDirect(payloads) {
|
|
1010
|
+
const batchRequest = {
|
|
1011
|
+
events: payloads
|
|
1012
|
+
};
|
|
1013
|
+
const response = await this.analyticsApi.analyticsBatchPost(this.storeId, this.readSecret, batchRequest, {
|
|
1014
|
+
headers: {
|
|
1015
|
+
'x-storeid': this.storeId,
|
|
1016
|
+
'x-storesecret': this.readSecret,
|
|
1017
|
+
}
|
|
1018
|
+
});
|
|
1019
|
+
if (response.status >= 400) {
|
|
1020
|
+
throw new Error(`Failed to send batch events: ${response.status}`);
|
|
1021
|
+
}
|
|
1022
|
+
}
|
|
1023
|
+
/**
|
|
1024
|
+
* Get browser context (sync if cached, otherwise returns null)
|
|
1025
|
+
* Use collectBrowserContext() for async collection
|
|
1026
|
+
*/
|
|
1027
|
+
getBrowserContext() {
|
|
1028
|
+
return this.cachedBrowserContext;
|
|
1029
|
+
}
|
|
1030
|
+
/**
|
|
1031
|
+
* Collect browser context asynchronously
|
|
1032
|
+
*/
|
|
1033
|
+
async collectBrowserContext() {
|
|
1034
|
+
this.cachedBrowserContext = await this.contextCollector.collect();
|
|
1035
|
+
return this.cachedBrowserContext;
|
|
1036
|
+
}
|
|
1037
|
+
/**
|
|
1038
|
+
* Build event payload with identifiers and browser context
|
|
642
1039
|
* Ensures user_id or anon_id is present, and sets correlation_id/search_id at top level
|
|
643
1040
|
*/
|
|
644
1041
|
buildEventPayload(event, context) {
|
|
@@ -665,6 +1062,53 @@ class SeekoraClient {
|
|
|
665
1062
|
if (payload.search_id && payload.metadata && !payload.metadata.search_id) {
|
|
666
1063
|
payload.metadata.search_id = payload.search_id;
|
|
667
1064
|
}
|
|
1065
|
+
// Add browser context if enabled and available
|
|
1066
|
+
if (this.enableContextCollection && this.cachedBrowserContext) {
|
|
1067
|
+
const ctx = this.cachedBrowserContext;
|
|
1068
|
+
// Screen/viewport dimensions
|
|
1069
|
+
payload.screen_width = ctx.screen_width;
|
|
1070
|
+
payload.screen_height = ctx.screen_height;
|
|
1071
|
+
payload.viewport_width = ctx.viewport_width;
|
|
1072
|
+
payload.viewport_height = ctx.viewport_height;
|
|
1073
|
+
payload.color_depth = ctx.color_depth;
|
|
1074
|
+
payload.pixel_ratio = ctx.pixel_ratio;
|
|
1075
|
+
// Device fingerprint (if available)
|
|
1076
|
+
if (ctx.device_fingerprint) {
|
|
1077
|
+
payload.device_fingerprint = ctx.device_fingerprint;
|
|
1078
|
+
}
|
|
1079
|
+
// Browser information
|
|
1080
|
+
payload.browser_name = ctx.browser_name;
|
|
1081
|
+
payload.browser_version = ctx.browser_version;
|
|
1082
|
+
payload.browser_language = ctx.browser_language;
|
|
1083
|
+
payload.timezone = ctx.timezone;
|
|
1084
|
+
payload.timezone_offset = ctx.timezone_offset;
|
|
1085
|
+
// Page context
|
|
1086
|
+
payload.page_url = ctx.page_url;
|
|
1087
|
+
payload.page_path = ctx.page_path;
|
|
1088
|
+
payload.page_title = ctx.page_title;
|
|
1089
|
+
payload.page_referrer = ctx.page_referrer;
|
|
1090
|
+
// UTM parameters (if present)
|
|
1091
|
+
if (ctx.utm_source)
|
|
1092
|
+
payload.utm_source = ctx.utm_source;
|
|
1093
|
+
if (ctx.utm_medium)
|
|
1094
|
+
payload.utm_medium = ctx.utm_medium;
|
|
1095
|
+
if (ctx.utm_campaign)
|
|
1096
|
+
payload.utm_campaign = ctx.utm_campaign;
|
|
1097
|
+
if (ctx.utm_term)
|
|
1098
|
+
payload.utm_term = ctx.utm_term;
|
|
1099
|
+
if (ctx.utm_content)
|
|
1100
|
+
payload.utm_content = ctx.utm_content;
|
|
1101
|
+
// Connection info (if available)
|
|
1102
|
+
if (ctx.connection_type)
|
|
1103
|
+
payload.connection_type = ctx.connection_type;
|
|
1104
|
+
if (ctx.connection_effective_type)
|
|
1105
|
+
payload.connection_effective_type = ctx.connection_effective_type;
|
|
1106
|
+
// Platform info
|
|
1107
|
+
payload.platform = ctx.platform;
|
|
1108
|
+
payload.is_mobile = ctx.is_mobile;
|
|
1109
|
+
payload.is_tablet = ctx.is_tablet;
|
|
1110
|
+
payload.is_touch_device = ctx.is_touch_device;
|
|
1111
|
+
}
|
|
668
1112
|
return payload;
|
|
669
1113
|
}
|
|
670
1114
|
/**
|
|
@@ -679,6 +1123,15 @@ class SeekoraClient {
|
|
|
679
1123
|
hasContext: !!context
|
|
680
1124
|
});
|
|
681
1125
|
const payload = this.buildEventPayload(event, context);
|
|
1126
|
+
// Use event queue if enabled
|
|
1127
|
+
if (this.enableEventQueue && this.eventQueue) {
|
|
1128
|
+
this.eventQueue.enqueue(payload);
|
|
1129
|
+
this.logger.verbose('Event queued for sending', {
|
|
1130
|
+
eventName: event.event_name,
|
|
1131
|
+
queueSize: this.eventQueue.size()
|
|
1132
|
+
});
|
|
1133
|
+
return;
|
|
1134
|
+
}
|
|
682
1135
|
try {
|
|
683
1136
|
this.logger.verbose('Sending analytics event', {
|
|
684
1137
|
endpoint: '/api/analytics/event',
|
|
@@ -719,13 +1172,22 @@ class SeekoraClient {
|
|
|
719
1172
|
this.logger.error('Batch size exceeds maximum', { count: events.length, max: 100 });
|
|
720
1173
|
throw new Error('Maximum 100 events per batch');
|
|
721
1174
|
}
|
|
1175
|
+
// Build full event payloads with identifiers
|
|
1176
|
+
const enrichedEvents = events.map(event => this.buildEventPayload(event, context));
|
|
1177
|
+
// Use event queue if enabled
|
|
1178
|
+
if (this.enableEventQueue && this.eventQueue) {
|
|
1179
|
+
this.eventQueue.enqueueBatch(enrichedEvents);
|
|
1180
|
+
this.logger.verbose('Events queued for sending', {
|
|
1181
|
+
count: events.length,
|
|
1182
|
+
queueSize: this.eventQueue.size()
|
|
1183
|
+
});
|
|
1184
|
+
return;
|
|
1185
|
+
}
|
|
722
1186
|
try {
|
|
723
1187
|
this.logger.verbose('Sending batch analytics events', {
|
|
724
1188
|
endpoint: '/api/analytics/batch',
|
|
725
1189
|
eventCount: events.length
|
|
726
1190
|
});
|
|
727
|
-
// Build full event payloads with identifiers
|
|
728
|
-
const enrichedEvents = events.map(event => this.buildEventPayload(event, context));
|
|
729
1191
|
const batchRequest = {
|
|
730
1192
|
events: enrichedEvents // Type assertion for extended payloads
|
|
731
1193
|
};
|
|
@@ -947,6 +1409,101 @@ class SeekoraClient {
|
|
|
947
1409
|
sessionId: this.sessionId,
|
|
948
1410
|
};
|
|
949
1411
|
}
|
|
1412
|
+
/**
|
|
1413
|
+
* Get the event queue instance (if enabled)
|
|
1414
|
+
*/
|
|
1415
|
+
getEventQueue() {
|
|
1416
|
+
return this.eventQueue;
|
|
1417
|
+
}
|
|
1418
|
+
/**
|
|
1419
|
+
* Flush the event queue manually
|
|
1420
|
+
* Useful when you want to ensure events are sent before page unload
|
|
1421
|
+
*/
|
|
1422
|
+
async flushEventQueue() {
|
|
1423
|
+
if (this.eventQueue) {
|
|
1424
|
+
await this.eventQueue.flush();
|
|
1425
|
+
}
|
|
1426
|
+
}
|
|
1427
|
+
/**
|
|
1428
|
+
* Get event queue statistics
|
|
1429
|
+
*/
|
|
1430
|
+
getEventQueueStats() {
|
|
1431
|
+
if (!this.eventQueue) {
|
|
1432
|
+
return null;
|
|
1433
|
+
}
|
|
1434
|
+
return {
|
|
1435
|
+
enabled: this.enableEventQueue,
|
|
1436
|
+
...this.eventQueue.getStats()
|
|
1437
|
+
};
|
|
1438
|
+
}
|
|
1439
|
+
/**
|
|
1440
|
+
* Identify a user and link their anonymous ID/fingerprint to their user ID
|
|
1441
|
+
* Call this when a user logs in to enable cross-device/session tracking
|
|
1442
|
+
*
|
|
1443
|
+
* @param userId - The authenticated user ID
|
|
1444
|
+
* @param traits - Optional user traits/properties
|
|
1445
|
+
*/
|
|
1446
|
+
async identify(userId, traits) {
|
|
1447
|
+
this.logger.verbose('Identifying user', { userId });
|
|
1448
|
+
// Update internal user ID
|
|
1449
|
+
this.userId = userId;
|
|
1450
|
+
// Collect current context for fingerprint
|
|
1451
|
+
const context = this.cachedBrowserContext || await this.collectBrowserContext();
|
|
1452
|
+
const payload = {
|
|
1453
|
+
user_id: userId,
|
|
1454
|
+
anon_id: this.anonId,
|
|
1455
|
+
session_id: this.sessionId,
|
|
1456
|
+
device_fingerprint: context?.device_fingerprint,
|
|
1457
|
+
traits,
|
|
1458
|
+
};
|
|
1459
|
+
try {
|
|
1460
|
+
const response = await axios_1.default.post(`${this.config.basePath}/api/analytics/identify`, payload, {
|
|
1461
|
+
headers: {
|
|
1462
|
+
'x-storeid': this.storeId,
|
|
1463
|
+
'x-storesecret': this.readSecret,
|
|
1464
|
+
},
|
|
1465
|
+
timeout: 10000,
|
|
1466
|
+
});
|
|
1467
|
+
this.logger.info('User identified successfully', {
|
|
1468
|
+
userId,
|
|
1469
|
+
linked: {
|
|
1470
|
+
anonId: !!this.anonId,
|
|
1471
|
+
fingerprint: !!context?.device_fingerprint,
|
|
1472
|
+
}
|
|
1473
|
+
});
|
|
1474
|
+
}
|
|
1475
|
+
catch (error) {
|
|
1476
|
+
this.logger.error('Failed to identify user', { userId, error: error.message });
|
|
1477
|
+
// Don't throw - identify failures shouldn't break the app
|
|
1478
|
+
}
|
|
1479
|
+
}
|
|
1480
|
+
/**
|
|
1481
|
+
* Alias an anonymous ID to a user ID
|
|
1482
|
+
* Use this when you want to link an external anonymous ID to a user
|
|
1483
|
+
*
|
|
1484
|
+
* @param anonId - The anonymous ID to alias
|
|
1485
|
+
* @param userId - The user ID to link to
|
|
1486
|
+
*/
|
|
1487
|
+
async alias(anonId, userId) {
|
|
1488
|
+
this.logger.verbose('Creating alias', { anonId, userId });
|
|
1489
|
+
const payload = {
|
|
1490
|
+
user_id: userId,
|
|
1491
|
+
anon_id: anonId,
|
|
1492
|
+
};
|
|
1493
|
+
try {
|
|
1494
|
+
await axios_1.default.post(`${this.config.basePath}/api/analytics/identify`, payload, {
|
|
1495
|
+
headers: {
|
|
1496
|
+
'x-storeid': this.storeId,
|
|
1497
|
+
'x-storesecret': this.readSecret,
|
|
1498
|
+
},
|
|
1499
|
+
timeout: 10000,
|
|
1500
|
+
});
|
|
1501
|
+
this.logger.info('Alias created successfully', { anonId, userId });
|
|
1502
|
+
}
|
|
1503
|
+
catch (error) {
|
|
1504
|
+
this.logger.error('Failed to create alias', { anonId, userId, error: error.message });
|
|
1505
|
+
}
|
|
1506
|
+
}
|
|
950
1507
|
/**
|
|
951
1508
|
* Handle errors consistently
|
|
952
1509
|
*/
|