diff --git a/posthog/api/site_app.py b/posthog/api/site_app.py index 6704ff0c7f534..1d692b943a084 100644 --- a/posthog/api/site_app.py +++ b/posthog/api/site_app.py @@ -7,6 +7,9 @@ from statshog.defaults.django import statsd from posthog.exceptions import generate_exception_response +from posthog.hogql import ast +from posthog.hogql.compiler.javascript import JavaScriptCompiler +from posthog.hogql.parser import parse_string_template from posthog.logging.timing import timed from posthog.plugins.site import get_site_config_from_schema, get_transpiled_site_source @@ -22,7 +25,48 @@ def get_site_app(request: HttpRequest, id: int, token: str, hash: str) -> HttpRe id = source_file.id source = source_file.source config = get_site_config_from_schema(source_file.config_schema, source_file.config) - response = f"{source}().inject({{config:{json.dumps(config)},posthog:window['__$$ph_site_app_{id}']}})" + + # Wrap in IIFE = Immediately Invoked Function Expression = to avoid polluting global scope + response = "(function() {\n\n" + + # Build a switch statement within a try/catch loop and a static dict + config_switch = "" + config_dict_items: list[str] = [] + + compiler = JavaScriptCompiler() + for key, value in config.items(): + key_string = json.dumps(str(key) or "") + if isinstance(value, str) and "{" in value: + base_code = compiler.visit(ast.ReturnStatement(expr=parse_string_template(value))) + config_switch += f"case {key_string}: {base_code};\n" + config_dict_items.append(f"{key_string}: getConfigKey({json.dumps(key)}, initial)") + else: + config_dict_items.append(f"{key_string}: {json.dumps(value)}") + + # Start with the STL functions + response += compiler.get_inlined_stl() + "\n" + + # This will be used by Hog code to access globals + response += "let __globals = {};\n" + response += "function __getGlobal(key) { return __globals[key] }\n" + + if config_switch: + response += ( + f"function getConfigKey(key, initial) {{ try {{ switch (key) {{\n\n///// calculated properties\n" + ) + response += config_switch + response += "\ndefault: return null; }\n" + response += "} catch (e) { if(!initial) {console.warn('[POSTHOG-JS] Unable to get config field', key, e);} return null } }\n" + + response += ( + f"function getConfig(globals, initial) {{ __globals = globals || {'{}'}; return {{\n\n///// config\n" + ) + response += ",\n".join(config_dict_items) + response += "\n\n} }\n" + + response += f"{source}().inject({{config:getConfig({'{}'}, true),getConfig:getConfig,posthog:window['__$$ph_site_app_{id}']}});" + + response += "\n\n})();" statsd.incr(f"posthog_cloud_raw_endpoint_success", tags={"endpoint": "site_app"}) return HttpResponse(content=response, content_type="application/javascript") diff --git a/posthog/hogql/compiler/javascript.py b/posthog/hogql/compiler/javascript.py index b5c46b3ba1955..14d405c2e6777 100644 --- a/posthog/hogql/compiler/javascript.py +++ b/posthog/hogql/compiler/javascript.py @@ -12,7 +12,7 @@ from posthog.hogql.visitor import Visitor # TODO: make sure _JS_GET_GLOBAL is unique! -_JS_GET_GLOBAL = "get_global" +_JS_GET_GLOBAL = "__getGlobal" _JS_KEYWORDS = ["var", "let", "const", "function", "typeof", "Error"] @@ -52,7 +52,7 @@ def create_javascript( supported_functions = supported_functions or set() compiler = JavaScriptCompiler(supported_functions, args, context, in_repl, locals) base_code = compiler.visit(expr) - import_code = import_stl_functions(compiler.inlined_stl) + import_code = compiler.get_inlined_stl() code = import_code + ("\n\n" if import_code else "") + base_code return CompiledJavaScript(code=code, locals=compiler.locals) @@ -95,6 +95,9 @@ def __init__( for arg in self.args: self._declare_local(arg) + def get_inlined_stl(self) -> str: + return import_stl_functions(self.inlined_stl) + def _start_scope(self): self.scope_depth += 1