Spaces:
Running on CPU Upgrade
Running on CPU Upgrade
| <html> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <style> | |
| body { | |
| margin: 0; | |
| padding: 0; | |
| overflow: hidden; | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <script> | |
| // UBI Client Implementation | |
| class UbiClient { | |
| constructor(ingestionEndpoint, awsRegion, debugMode) { | |
| this.ingestionEndpoint = ingestionEndpoint; | |
| this.awsRegion = awsRegion; | |
| this.debugMode = debugMode; | |
| this.applicationName = 'os-product-catalog'; | |
| // Initialize IDs | |
| this.clientId = this.getOrCreateClientId(); | |
| this.sessionId = this.getOrCreateSessionId(); | |
| this.currentQueryId = 'browsing'; | |
| if (this.debugMode) { | |
| console.log('UBI Client initialized:', { | |
| clientId: this.clientId, | |
| sessionId: this.sessionId, | |
| ingestionEndpoint: this.ingestionEndpoint | |
| }); | |
| } | |
| } | |
| /** | |
| * Generate a UUID v4 | |
| * Uses crypto.randomUUID() with fallback for older browsers | |
| */ | |
| generateUUID() { | |
| // Modern browsers support crypto.randomUUID() | |
| if (typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function') { | |
| return crypto.randomUUID(); | |
| } | |
| // Fallback implementation for older browsers | |
| return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) { | |
| const r = Math.random() * 16 | 0; | |
| const v = c === 'x' ? r : (r & 0x3 | 0x8); | |
| return v.toString(16); | |
| }); | |
| } | |
| /** | |
| * Get or create Client_ID from localStorage | |
| * Client_ID persists across browser sessions | |
| */ | |
| getOrCreateClientId() { | |
| const storageKey = 'ubi_client_id'; | |
| try { | |
| let clientId = localStorage.getItem(storageKey); | |
| if (!clientId) { | |
| clientId = this.generateUUID(); | |
| localStorage.setItem(storageKey, clientId); | |
| if (this.debugMode) { | |
| console.log('Created new Client_ID:', clientId); | |
| } | |
| } else { | |
| if (this.debugMode) { | |
| console.log('Retrieved existing Client_ID:', clientId); | |
| } | |
| } | |
| return clientId; | |
| } catch (error) { | |
| console.error('Error accessing localStorage for Client_ID:', error); | |
| // Return a temporary UUID if localStorage is not available | |
| return this.generateUUID(); | |
| } | |
| } | |
| /** | |
| * Get or create Session_ID from sessionStorage | |
| * Session_ID is unique per browser tab and cleared when tab closes | |
| */ | |
| getOrCreateSessionId() { | |
| const storageKey = 'ubi_session_id'; | |
| try { | |
| let sessionId = sessionStorage.getItem(storageKey); | |
| if (!sessionId) { | |
| sessionId = this.generateUUID(); | |
| sessionStorage.setItem(storageKey, sessionId); | |
| if (this.debugMode) { | |
| console.log('Created new Session_ID:', sessionId); | |
| } | |
| } else { | |
| if (this.debugMode) { | |
| console.log('Retrieved existing Session_ID:', sessionId); | |
| } | |
| } | |
| return sessionId; | |
| } catch (error) { | |
| console.error('Error accessing sessionStorage for Session_ID:', error); | |
| // Return a temporary UUID if sessionStorage is not available | |
| return this.generateUUID(); | |
| } | |
| } | |
| /** | |
| * Generate a new Query_ID and store it in sessionStorage | |
| * Called when a new search query is performed | |
| */ | |
| generateQueryId() { | |
| const queryId = this.generateUUID(); | |
| this.currentQueryId = queryId; | |
| try { | |
| sessionStorage.setItem('ubi_query_id', queryId); | |
| if (this.debugMode) { | |
| console.log('Generated new Query_ID:', queryId); | |
| } | |
| } catch (error) { | |
| console.error('Error storing Query_ID in sessionStorage:', error); | |
| } | |
| return queryId; | |
| } | |
| /** | |
| * Get the current Query_ID from sessionStorage | |
| * Returns 'browsing' if no query has been performed | |
| */ | |
| getCurrentQueryId() { | |
| try { | |
| const queryId = sessionStorage.getItem('ubi_query_id'); | |
| return queryId || 'browsing'; | |
| } catch (error) { | |
| console.error('Error retrieving Query_ID from sessionStorage:', error); | |
| return 'browsing'; | |
| } | |
| } | |
| /** | |
| * Generate ISO 8601 timestamp | |
| */ | |
| getTimestamp() { | |
| return new Date().toISOString(); | |
| } | |
| /** | |
| * Validate required fields in query data | |
| */ | |
| validateQueryData(data) { | |
| const required = ['application', 'query_id', 'client_id', 'user_query', 'object_id_field', 'timestamp']; | |
| for (const field of required) { | |
| if (!data[field] && data[field] !== '') { | |
| console.error(`UBI query validation failed: missing ${field}`); | |
| return false; | |
| } | |
| } | |
| return true; | |
| } | |
| /** | |
| * Validate required fields in event data | |
| */ | |
| validateEventData(data) { | |
| const required = ['application', 'action_name', 'query_id', 'client_id', 'session_id', 'user_id', 'timestamp']; | |
| for (const field of required) { | |
| if (data[field] === undefined || data[field] === null) { | |
| console.error(`UBI event validation failed: missing ${field}`); | |
| return false; | |
| } | |
| } | |
| // Validate position.ordinal exists for events with object_id | |
| if (data.event_attributes && data.event_attributes.object && data.event_attributes.object.object_id) { | |
| if (!data.event_attributes.position || typeof data.event_attributes.position.ordinal !== 'number') { | |
| console.error('UBI event validation failed: missing position.ordinal for event with object_id'); | |
| return false; | |
| } | |
| } | |
| return true; | |
| } | |
| /** | |
| * Format UBI query data structure | |
| */ | |
| formatUbiQuery(queryText, queryId) { | |
| const data = { | |
| application: this.applicationName, | |
| query_id: queryId || this.currentQueryId, | |
| client_id: this.clientId, | |
| user_query: queryText || '', | |
| object_id_field: 'id', | |
| query_attributes: {}, | |
| timestamp: this.getTimestamp() | |
| }; | |
| return data; | |
| } | |
| /** | |
| * Format UBI event data structure | |
| */ | |
| formatUbiEvent(actionName, queryId, objectId, eventAttributes, message) { | |
| const data = { | |
| application: this.applicationName, | |
| action_name: actionName, | |
| query_id: queryId || this.currentQueryId, | |
| client_id: this.clientId, | |
| session_id: this.sessionId, | |
| user_id: '', | |
| timestamp: this.getTimestamp(), | |
| message_type: 'INFO', | |
| message: message || '' | |
| }; | |
| // Add event_attributes if provided | |
| if (eventAttributes) { | |
| data.event_attributes = eventAttributes; | |
| // Ensure object_id_field is set if object exists | |
| if (eventAttributes.object && eventAttributes.object.object_id) { | |
| if (!eventAttributes.object.object_id_field) { | |
| eventAttributes.object.object_id_field = 'id'; | |
| } | |
| // Ensure position.ordinal exists for events with object_id | |
| if (!eventAttributes.position) { | |
| eventAttributes.position = { ordinal: 0 }; | |
| } else if (typeof eventAttributes.position.ordinal !== 'number') { | |
| eventAttributes.position.ordinal = 0; | |
| } | |
| } | |
| } else if (objectId) { | |
| // If objectId is provided but no eventAttributes, create minimal structure | |
| data.event_attributes = { | |
| object: { | |
| object_id: objectId, | |
| object_id_field: 'id' | |
| }, | |
| position: { | |
| ordinal: 0 | |
| } | |
| }; | |
| } | |
| return data; | |
| } | |
| /** | |
| * Send data to OpenSearch Ingestion Service | |
| * @param {Object} data - The UBI data to send | |
| * @param {string} endpoint - Either 'ubi_queries' or 'ubi_events' | |
| * | |
| * Requirements: 9.5, 9.6, 12.1, 12.2, 12.3 | |
| */ | |
| async sendToIngestion(data, endpoint) { | |
| if (!this.ingestionEndpoint) { | |
| console.error('UBI tracking disabled: ingestion endpoint not configured'); | |
| return; | |
| } | |
| // Wrap data in array format as required by ingestion service | |
| const payload = [data]; | |
| const url = `${this.ingestionEndpoint}/${endpoint}`; | |
| if (this.debugMode) { | |
| console.log(`Sending to ${endpoint}:`, JSON.stringify(data, null, 2)); | |
| } | |
| try { | |
| const response = await fetch(url, { | |
| method: 'POST', | |
| headers: { | |
| 'Content-Type': 'application/json' | |
| }, | |
| body: JSON.stringify(payload) | |
| }); | |
| if (!response.ok) { | |
| console.error(`UBI tracking failed: ${response.status} ${response.statusText}`); | |
| console.error('UBI: Request will not be retried. Application continues normally.'); | |
| if (this.debugMode) { | |
| try { | |
| const errorText = await response.text(); | |
| console.error('Error response:', errorText); | |
| } catch (e) { | |
| console.error('Could not read error response:', e); | |
| } | |
| } | |
| } else { | |
| if (this.debugMode) { | |
| console.log(`UBI ${endpoint} tracked successfully`); | |
| } | |
| } | |
| } catch (error) { | |
| // Network error or other fetch failure | |
| console.error('UBI tracking network error:', error.message || error); | |
| console.error('UBI: Request will not be retried. Application continues normally.'); | |
| // Silent failure - do not disrupt user experience | |
| // Do not retry failed requests to avoid overwhelming the service | |
| } | |
| } | |
| /** | |
| * Track a search query | |
| */ | |
| async trackQuery(queryText, queryId) { | |
| try { | |
| const data = this.formatUbiQuery(queryText, queryId); | |
| if (!this.validateQueryData(data)) { | |
| console.error('Query data validation failed, skipping tracking'); | |
| return; | |
| } | |
| await this.sendToIngestion(data, 'ubi_queries'); | |
| } catch (error) { | |
| console.error('Error tracking query:', error); | |
| } | |
| } | |
| /** | |
| * Track a user event | |
| */ | |
| async trackEvent(actionName, queryId, objectId, eventAttributes, message) { | |
| try { | |
| const data = this.formatUbiEvent(actionName, queryId, objectId, eventAttributes, message); | |
| if (!this.validateEventData(data)) { | |
| console.error('Event data validation failed, skipping tracking'); | |
| return; | |
| } | |
| await this.sendToIngestion(data, 'ubi_events'); | |
| } catch (error) { | |
| console.error('Error tracking event:', error); | |
| } | |
| } | |
| } | |
| // Initialize UBI client when component loads | |
| let ubiClient = null; | |
| // This will be called from Python to initialize the client | |
| function initializeUbiClient(config) { | |
| try { | |
| ubiClient = new UbiClient( | |
| config.ingestionEndpoint, | |
| config.awsRegion, | |
| config.debugMode | |
| ); | |
| if (config.debugMode) { | |
| console.log('UBI Client ready'); | |
| } | |
| // Send Client_ID and Session_ID back to Streamlit | |
| // This allows Python to use these IDs when tracking via proxy | |
| if (window.parent && window.parent.Streamlit) { | |
| window.parent.Streamlit.setComponentValue({ | |
| client_id: ubiClient.clientId, | |
| session_id: ubiClient.sessionId | |
| }); | |
| } | |
| } catch (error) { | |
| console.error('Failed to initialize UBI Client:', error); | |
| } | |
| } | |
| // Expose initialization function to parent window | |
| window.initializeUbiClient = initializeUbiClient; | |
| // Set up Streamlit component communication | |
| if (window.parent && window.parent.Streamlit) { | |
| window.parent.Streamlit.setComponentReady(); | |
| } | |
| </script> | |
| </body> | |
| </html> | |