-
Notifications
You must be signed in to change notification settings - Fork 2
Add load tests for conversational analytics #23
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,160 @@ | ||
| <!DOCTYPE html> | ||
| <html lang="en"> | ||
| <head> | ||
| <meta charset="UTF-8"> | ||
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | ||
| <link rel="icon" href="data:,"> | ||
| <title>Cookieless Embed Conversational Analytics</title> | ||
| <style> | ||
| iframe { | ||
| width: 100%; | ||
| height: 95vh; | ||
| border: 1px solid #ccc; | ||
| } | ||
| </style> | ||
| </head> | ||
| <body> | ||
| <h1>Cookieless Embed Conversational Analytics</h1> | ||
| <div id="embed-container"></div> | ||
|
|
||
| <script> | ||
| if ({{debug}}) { | ||
| localStorage.setItem("debug", "looker:chatty:*") | ||
| } | ||
| const embedContainer = document.getElementById('embed-container'); | ||
| let sessionReferenceToken; | ||
| let apiToken; | ||
| let navigationToken; | ||
| let connected = false; | ||
| let acquireData; | ||
|
|
||
| async function acquireSession() { | ||
| console.log("acquireSession() called at " + new Date().toISOString()); | ||
| const resp = await fetch('/acquire-embed-session'); | ||
| if (!resp.ok) { | ||
| console.error('acquire-embed-session failed', { resp }); | ||
| throw new Error(`acquire-embed-session failed: ${resp.status} ${resp.statusText}`); | ||
| } | ||
| return await resp.json(); | ||
| } | ||
|
|
||
| async function getCookielessLoginUrl(embedUrl) { | ||
| console.log("getCookielessLoginUrl() called at " + new Date().toISOString()); | ||
| acquireData = await acquireSession(); | ||
| const { authentication_token, navigation_token, session_reference_token, api_token } = acquireData; | ||
| console.log("Creating iframe URL with authentication token:", authentication_token); | ||
| sessionReferenceToken = session_reference_token; | ||
| apiToken = api_token; | ||
| navigationToken = navigation_token; | ||
|
|
||
| const path = embedUrl.startsWith('/embed') ? embedUrl : `/embed${embedUrl}`; | ||
| const query_params = { | ||
| embed_domain: location.origin, | ||
| embed_navigation_token: navigation_token, | ||
| }; | ||
| const encoded_path = encodeURIComponent( | ||
| path + | ||
| "?" + | ||
| Object.entries(query_params) | ||
| .map(([key, value]) => `${key}=${value}`) | ||
| .join("&") | ||
| ); | ||
|
|
||
| const iframe_url = new URL( | ||
| `{{LOOKER_HOST}}/login/embed/${encoded_path}` | ||
| ); | ||
| return `${iframe_url.toString()}?embed_authentication_token=${authentication_token}` | ||
| } | ||
|
|
||
| function setupMessageListener() { | ||
| window.addEventListener('message', (event) => { | ||
| console.log(`Received message from iframe with data: ${JSON.stringify(event.data)}`); | ||
| let data; | ||
| let message; | ||
| try { | ||
| data = JSON.parse(event.data); | ||
| } catch (e) { | ||
| return; | ||
| } | ||
|
|
||
| if (data.type === 'session:tokens:request') { | ||
| if (connected) { | ||
| generateEmbedTokens(event.source, event.origin); | ||
| } else { | ||
| const iframe = document.getElementById('looker-embed') | ||
| message = JSON.stringify({ | ||
| type: "session:tokens", | ||
| api_token: acquireData.api_token, | ||
| api_token_ttl: acquireData.api_token_ttl, | ||
| navigation_token: acquireData.navigation_token, | ||
| navigation_token_ttl: acquireData.navigation_token_ttl, | ||
| session_reference_token_ttl: acquireData.session_reference_token_ttl | ||
| }) | ||
| console.log(`sending message: ${message} from ${event.origin}`) | ||
| iframe.contentWindow.postMessage( | ||
| message, | ||
| event.origin | ||
| ); | ||
| connected = true; | ||
| } | ||
| } | ||
| }); | ||
| } | ||
|
|
||
| const generateEmbedTokens = async (source, origin) => { | ||
| const response = await fetch("/generate-embed-tokens", { | ||
| method: "POST", | ||
| headers: { | ||
| 'Content-Type': 'application/json' | ||
| }, | ||
| body: JSON.stringify({ | ||
| session_reference_token: sessionReferenceToken, | ||
| api_token: apiToken, | ||
| navigation_token: navigationToken | ||
| }) | ||
| }); | ||
| const embedTokenData = await response.json(); | ||
| console.log("Received new tokens:", JSON.stringify(embedTokenData)); | ||
| const iframe = document.getElementById('looker-embed') | ||
| iframe.contentWindow.postMessage( | ||
| JSON.stringify({ | ||
| type: "session:tokens", | ||
| api_token: embedTokenData.api_token, | ||
| api_token_ttl: embedTokenData.api_token_ttl, | ||
| navigation_token: embedTokenData.navigation_token, | ||
| navigation_token_ttl: embedTokenData.navigation_token_ttl, | ||
| session_reference_token_ttl: embedTokenData.session_reference_token_ttl | ||
| }), | ||
| origin | ||
| ); | ||
| }; | ||
|
|
||
| async function embedConversationalAnalytics() { | ||
| const agentId = '{{AGENT_ID}}'; | ||
| const conversationId = '{{CONVERSATION_ID}}'; | ||
|
|
||
| let embedUrl = '/conversations'; | ||
| if (conversationId) { | ||
| embedUrl = `/conversations/${conversationId}`; | ||
| } else if (agentId) { | ||
| embedUrl = `/agents/${agentId}`; | ||
| } | ||
|
|
||
| try { | ||
| const loginUrl = await getCookielessLoginUrl(embedUrl); | ||
| console.log("Login URL:", loginUrl); | ||
| const iframe = document.createElement('iframe'); | ||
| iframe.id = "looker-embed" | ||
| iframe.src = loginUrl; | ||
| embedContainer.appendChild(iframe); | ||
| setupMessageListener(); | ||
| } catch (error) { | ||
| console.error('Failed to embed conversational analytics:', error); | ||
| embedContainer.innerHTML = '<p>Failed to load embedded content. See console for details.</p>'; | ||
| } | ||
| } | ||
|
|
||
| embedConversationalAnalytics(); | ||
| </script> | ||
| </body> | ||
| </html> |
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
| @@ -0,0 +1,162 @@ | ||||||
| import os | ||||||
| import json | ||||||
| from http.server import BaseHTTPRequestHandler, HTTPServer | ||||||
| from pathlib import Path | ||||||
| import looker_sdk | ||||||
| from looker_sdk import models40 | ||||||
| from lkr.load_test.utils import ( | ||||||
| MAX_SESSION_LENGTH, | ||||||
| PERMISSIONS, | ||||||
| get_user_id, | ||||||
| format_attributes, | ||||||
| ) | ||||||
| import sys | ||||||
|
|
||||||
| class CookielessEmbedHandler(BaseHTTPRequestHandler): | ||||||
| def __init__(self, *args, debug=False, port=None, **kwargs): | ||||||
| self.debug = debug | ||||||
| self.sdk = looker_sdk.init40() | ||||||
| self.port = port | ||||||
| super().__init__(*args, **kwargs) | ||||||
|
|
||||||
| def log_message(self, format, *args): | ||||||
| pass | ||||||
|
|
||||||
| def do_GET(self): | ||||||
| if self.path == '/': | ||||||
| self.send_response(200) | ||||||
| self.send_header('Content-type', 'text/html') | ||||||
| self.end_headers() | ||||||
| html_path = Path(__file__).parent / "embed_container.html" | ||||||
| with open(html_path, "r") as f: | ||||||
| html_content = f.read() | ||||||
|
|
||||||
| looker_host = os.environ.get("LOOKERSDK_BASE_URL", "") | ||||||
| agent_id = os.environ.get("AGENT_ID", "") | ||||||
| conversation_id = os.environ.get("CONVERSATION_ID", "") | ||||||
|
|
||||||
| html_content = html_content.replace("{{LOOKER_HOST}}", looker_host) | ||||||
| html_content = html_content.replace("{{AGENT_ID}}", agent_id) | ||||||
| html_content = html_content.replace("{{CONVERSATION_ID}}", conversation_id) | ||||||
| html_content = html_content.replace("{{debug}}", str(self.debug).lower()) | ||||||
|
|
||||||
| self.wfile.write(html_content.encode("utf-8")) | ||||||
| elif self.path == '/acquire-embed-session': | ||||||
| self.send_response(200) | ||||||
| self.send_header('Content-type', 'application/json') | ||||||
| self.send_header('Cache-Control', 'no-store') | ||||||
| self.end_headers() | ||||||
|
|
||||||
| user_id = get_user_id() | ||||||
|
|
||||||
| models_str = os.environ.get("MODELS", "") | ||||||
| models = models_str.split(",") if models_str else [] | ||||||
| group_ids_str = os.environ.get("GROUP_IDS", "") | ||||||
| group_ids = group_ids_str.split(",") if group_ids_str else [] | ||||||
| external_group_id = os.environ.get("EXTERNAL_GROUP_ID") | ||||||
|
|
||||||
| attributes_str = os.environ.get("ATTRIBUTES", "[]") | ||||||
| attributes_list = json.loads(attributes_str) | ||||||
| user_attributes = format_attributes(attributes_list) | ||||||
| first_name = os.environ.get("FIRST_NAME", "Cookieless Embed") | ||||||
|
|
||||||
| user_session = models40.EmbedCookielessSessionAcquire( | ||||||
| first_name=first_name, | ||||||
| last_name=user_id, | ||||||
| external_user_id=user_id, | ||||||
| session_length=3600, | ||||||
| permissions=PERMISSIONS + ["chat_with_explore"], | ||||||
| models=models, | ||||||
| group_ids=group_ids, | ||||||
| external_group_id=external_group_id, | ||||||
| user_attributes=user_attributes, | ||||||
| embed_domain=f"http://127.0.0.1:{self.port}" | ||||||
| ) | ||||||
|
|
||||||
| try: | ||||||
| response = self.sdk.acquire_embed_cookieless_session( | ||||||
| body=user_session, | ||||||
| transport_options={'headers':{'User-Agent': self.headers.get('User-Agent')}} | ||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If the It is safer to defensively check if the
Suggested change
|
||||||
| ) | ||||||
| self.wfile.write(json.dumps({ | ||||||
| 'api_token': response.api_token, | ||||||
| 'api_token_ttl': response.api_token_ttl, | ||||||
| 'authentication_token': response.authentication_token, | ||||||
| 'authentication_token_ttl': response.authentication_token_ttl, | ||||||
| 'navigation_token': response.navigation_token, | ||||||
| 'navigation_token_ttl': response.navigation_token_ttl, | ||||||
| 'session_reference_token': response.session_reference_token, | ||||||
| 'session_reference_token_ttl': response.session_reference_token_ttl, | ||||||
| }).encode('utf-8')) | ||||||
| except Exception as e: | ||||||
| self.send_response(500) | ||||||
| self.end_headers() | ||||||
| self.wfile.write(json.dumps({'error': str(e)}).encode('utf-8')) | ||||||
| else: | ||||||
| self.send_response(404) | ||||||
| self.end_headers() | ||||||
|
|
||||||
| def do_POST(self): | ||||||
| if self.path == '/generate-embed-tokens': | ||||||
| content_length = int(self.headers['Content-Length']) | ||||||
| post_data = self.rfile.read(content_length) | ||||||
| data = json.loads(post_data) | ||||||
| session_reference_token = data.get('session_reference_token') | ||||||
| api_token = data.get('api_token') | ||||||
| navigation_token = data.get('navigation_token') | ||||||
|
|
||||||
| if not session_reference_token or not api_token or not navigation_token: | ||||||
| self.send_response(400) | ||||||
| self.send_header('Content-type', 'application/json') | ||||||
| self.end_headers() | ||||||
| self.wfile.write(json.dumps({'error': 'session_reference_token, api_token, and navigation_token are required'}).encode('utf-8')) | ||||||
| return | ||||||
|
|
||||||
| try: | ||||||
| session_information = models40.EmbedCookielessSessionGenerateTokens( | ||||||
| session_reference_token=session_reference_token, | ||||||
| api_token=api_token, | ||||||
| navigation_token=navigation_token | ||||||
| ) | ||||||
| response = self.sdk.generate_tokens_for_cookieless_session( | ||||||
| body=session_information, | ||||||
| transport_options={'headers':{'User-Agent': self.headers.get('User-Agent')}} | ||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If the It is safer to defensively check if the
Suggested change
|
||||||
| ) | ||||||
| self.send_response(200) | ||||||
| self.send_header('Content-type', 'application/json') | ||||||
| self.end_headers() | ||||||
| self.wfile.write(json.dumps({ | ||||||
| 'api_token': response.api_token, | ||||||
| 'api_token_ttl': response.api_token_ttl, | ||||||
| 'navigation_token': response.navigation_token, | ||||||
| 'navigation_token_ttl': response.navigation_token_ttl, | ||||||
| 'session_reference_token': response.session_reference_token, | ||||||
| 'session_reference_token_ttl': response.session_reference_token_ttl, | ||||||
| }).encode('utf-8')) | ||||||
| except Exception as e: | ||||||
| print(e) | ||||||
| self.send_response(500) | ||||||
| self.end_headers() | ||||||
| self.wfile.write(json.dumps({'error': str(e)}).encode('utf-8')) | ||||||
| else: | ||||||
| self.send_response(404) | ||||||
| self.end_headers() | ||||||
|
|
||||||
| def run_server(port=8080, debug=False): | ||||||
| def handler(*args, **kwargs): | ||||||
| CookielessEmbedHandler(*args, debug=debug, port=port, **kwargs) | ||||||
|
|
||||||
| server_address = ('' , port) | ||||||
| httpd = HTTPServer(server_address, handler) | ||||||
| httpd.serve_forever() | ||||||
|
|
||||||
| if __name__ == '__main__': | ||||||
| port = 8080 | ||||||
| debug = "--debug" in sys.argv | ||||||
| if len(sys.argv) > 1 and not sys.argv[1].startswith("--"): | ||||||
| try: | ||||||
| port = int(sys.argv[1]) | ||||||
| except ValueError: | ||||||
| print("Invalid port number", file=sys.stderr) | ||||||
| sys.exit(1) | ||||||
| run_server(port, debug) | ||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Initializing the Looker SDK (
looker_sdk.init40()) inside the__init__method ofCookielessEmbedHandlermeans a new SDK instance is created for every single HTTP request (sinceHTTPServerinstantiates the handler class per request). This is extremely expensive and will severely limit the throughput of the embed server during load tests.Instead, initialize the SDK once (e.g., as a class-level attribute) and reuse it across requests.