OpenSearch-AI / search_personalization /ubi_component_template.html
prasadnu's picture
feat: add Search Personalization demo module
b4d5c9a
Raw
History Blame
16 kB
<!DOCTYPE html>
<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>