Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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()

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

Initializing the Looker SDK (looker_sdk.init40()) inside the __init__ method of CookielessEmbedHandler means a new SDK instance is created for every single HTTP request (since HTTPServer instantiates 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.

Suggested change
self.sdk = looker_sdk.init40()
self.debug = debug
if not hasattr(CookielessEmbedHandler, "_shared_sdk"):
CookielessEmbedHandler._shared_sdk = looker_sdk.init40()
self.sdk = CookielessEmbedHandler._shared_sdk

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')}}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

If the User-Agent header is missing from the incoming request, self.headers.get('User-Agent') will return None. Passing None as a header value in transport_options can cause a TypeError in some HTTP clients or underlying libraries.

It is safer to defensively check if the User-Agent is present or default to an empty string.

Suggested change
transport_options={'headers':{'User-Agent': self.headers.get('User-Agent')}}
transport_options={'headers':{'User-Agent': self.headers.get('User-Agent') or ''}}

)
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')}}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

If the User-Agent header is missing from the incoming request, self.headers.get('User-Agent') will return None. Passing None as a header value in transport_options can cause a TypeError in some HTTP clients or underlying libraries.

It is safer to defensively check if the User-Agent is present or default to an empty string.

Suggested change
transport_options={'headers':{'User-Agent': self.headers.get('User-Agent')}}
transport_options={'headers':{'User-Agent': self.headers.get('User-Agent') or ''}}

)
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)
Loading