Is Supademo: AI interactive demos in seconds safe?
Supademo records full DOM snapshots of pages you capture and sends debug logs with the active tab URL and account details to a third-party logger.
When you record an interactive demo, Supademo serializes the captured page's full DOM, including form field values (passwords and user-masked fields excluded), computed styles and canvas images, and uploads it to its media servers. Separately, its injected content scripts batch debug/info/warn logs and POST them every five seconds to the logging vendor BetterStack, attaching the recorded tab's URL plus the signed-in user's id, workspace id and name. Both behaviors occur only on tabs the user actively records.
Who publishes itSupademo, Inc. - no other listings under this identity
Supademo, Inc. - no other listings under this identity
What this publisher told the store about itself, and the other listings that told it the same thing.
AI-generated. Findings may contain errors. Those marked Verified have been manually reviewed.
Publishers can request a review.
Findings
Recording Logs Send Tab URLs to BetterStack
We observed the extension post recording-log batches to BetterStack during an active Supademo recording.
Log entries include the recorded tab URL plus signed-in account/workspace context (userId, workspaceId, name), seen in the JSON body.
What actually happens
You start recording or redacting a Supademo demo in the current tab.
That action causes the extension to run content scripts inside the page being recorded.
The content script queues log entries that include the current tab URL and account workspace context.
A timer posts the queued entries to BetterStack about every five seconds while entries are present.
When this fires
Every 5 seconds
Log entries are held in an in-memory queue and sent in batches about every five seconds when there is something to send.
Fields the logger adds to each BetterStack batch
- Recorded tab URLhttps://app.example.com/admin/users?workspace=acme (illustrative)
Shows the full page address open in the tab being recorded, which can expose the application, route, and query text you were viewing.
- Signed-in accountuserId: user_2N7fK9aQ, name: Jane Doe (illustrative)
Links the log entry to the Supademo account used during the recording session.
- Workspace contextworkspaceId: ws_8c2fb1e4 (illustrative)
Connects the recording activity to the workspace stored by the extension.
- Browser tab listid: 123, isActive: true, windowId: 7 (illustrative)
Adds browser context about open tabs, including which tab is active and the window it belongs to.
- Runtime environmentbrowser: Chrome, os: Mac, env: production, version: 7.4.8 (illustrative)
Adds browser, operating system, environment, and extension version details to the same log entry.
Captured request
Observed during dynamic analysis as a BetterStack log-ingestion POST; the available evidence confirms the JSON body contained context.auth and context.activeTab.url fields.
- Content-Type
- application/json
- Authorization
- Bearer <redacted>
The code that does this
The logger queues account and tab context, then posts the batch to BetterStack
content.js deobfuscated logger and sender
content-scripts/content.jsasync function Hz() { if (Dl.length === 0) return; const e = "https://s1270069.eu-nbg-2.betterstackdata.com", t = "<redacted>", n = JSON.stringify(Dl); Nz(); try { await xv(e, { method: "POST", headers: { "Content-Type": "application/json", Authorization: `Bearer ${t}` }, body: n }) } catch {} }function Bz(e) { e.addReporter({ log(t, n) { const s = tn(t.type).with("trace", () => "debug").with("debug", () => "debug").with("info", () => "info").with("success", () => "info").with("warn", () => "warn").with("error", () => "warn").with("fatal", () => "error").otherwise(() => "debug"), r = `${t.date.toISOString()}`, o = t.args[1], i = (o == null ? void 0 : o.slug) ?? null, a = hg(o) ? sl(t.args[1], ["slug"]) : o, l = t.args[0]; Promise.all([xi.getValue(), E1.getValue(), po.getValue(), Il.getValue(), Pr.getValue(), Iz(), Lz(), Vz()]).then(([u, c, d, h, f, w, y, v]) => { Dl.push({ level: s, tag: n.options.defaults.tag ?? "Unknown", message: l, slug: i, dt: r, extraData: a ?? void 0, context: { slug: i, health: u, auth: c, settings: d, demo: h, activeTab: { id: w.id, url: w.url }, tabs: y.map(b => ({ id: b.id ?? null, isActive: b.active, windowId: b.windowId })), redactor: f }, environment: { os: v, browser: Pz(), env: "production", version: "7.4.8" } }) }) } }), e.addReporter({ log(t, n) { var i; const s = tn(t.type).with("trace", () => "debug").with("debug", () => "debug").with("info", () => "debug").with("warn", () => "warning").with("error", () => "error").with("fatal", () => "fatal").otherwise(() => "debug"), r = ((i = t.args[0]) == null ? void 0 : i.message) || t.args[0], o = t.args[1]; if (s === "fatal") Promise.all([E1.getValue(), po.getValue(), Il.getValue(), Pr.getValue()]).then(([a, l, u, c]) => { tr.setUser({ id: (a == null ? void 0 : a.userId) ?? "UNAUTHENTICATED", username: (a == null ? void 0 : a.name) ?? "UNAUTHENTICATED" }), tr.setTags({ demoId: u.id, userId: (a == null ? void 0 : a.userId) ?? "UNAUTHENTICATED", workspaceId: (a == null ? void 0 : a.workspaceId) ?? "UNAUTHENTICATED", recordingType: u.type, isAddingSteps: u.isAddingStepsInBetween, isRedactorEnabled: c.mode !== "none" }), tr.setContext("settings", { ...l }), tr.setContext("demo", { ...u }), tr.setContext("redactor", { ...c }); const d = new Error(o); d.name = r, tr.captureException(d) }).catch(a => { console.error("Error while getting data for Sentry", a) }); else { const a = hg(o) ? o : { extraData: o }, l = sl(a, ["slug"]); tr.addBreadcrumb({ data: l, message: r, level: s, category: n.options.defaults.tag }) } } }) }redactor.js deobfuscated logger and sender
content-scripts/redactor.jsasync function ph() { if (zn.length === 0) return; const e = "https://s1270069.eu-nbg-2.betterstackdata.com", t = "<redacted>", n = JSON.stringify(zn); dh(); try { await wa(e, { method: "POST", headers: { "Content-Type": "application/json", Authorization: `Bearer ${t}` }, body: n }) } catch {} }function mh(e) { e.addReporter({ log(t, n) { const r = wn(t.type).with("trace", () => "debug").with("debug", () => "debug").with("info", () => "info").with("success", () => "info").with("warn", () => "warn").with("error", () => "warn").with("fatal", () => "error").otherwise(() => "debug"), s = `${t.date.toISOString()}`, o = t.args[1], i = (o == null ? void 0 : o.slug) ?? null, a = So(o) ? bn(t.args[1], ["slug"]) : o, c = t.args[0]; Promise.all([oh.getValue(), Na.getValue(), ka.getValue(), $a.getValue(), en.getValue(), yh(), gh(), rh()]).then(([l, u, f, h, d, E, g, p]) => { zn.push({ level: r, tag: n.options.defaults.tag ?? "Unknown", message: c, slug: i, dt: s, extraData: a ?? void 0, context: { slug: i, health: l, auth: u, settings: f, demo: h, activeTab: { id: E.id, url: E.url }, tabs: g.map(m => ({ id: m.id ?? null, isActive: m.active, windowId: m.windowId })), redactor: d }, environment: { os: p, browser: sh(), env: "production", version: "7.4.8" } }) }) } }), e.addReporter({ log(t, n) { var i; const r = wn(t.type).with("trace", () => "debug").with("debug", () => "debug").with("info", () => "debug").with("warn", () => "warning").with("error", () => "error").with("fatal", () => "fatal").otherwise(() => "debug"), s = ((i = t.args[0]) == null ? void 0 : i.message) || t.args[0], o = t.args[1]; if (r === "fatal") Promise.all([Na.getValue(), ka.getValue(), $a.getValue(), en.getValue()]).then(([a, c, l, u]) => { nt.setUser({ id: (a == null ? void 0 : a.userId) ?? "UNAUTHENTICATED", username: (a == null ? void 0 : a.name) ?? "UNAUTHENTICATED" }), nt.setTags({ demoId: l.id, userId: (a == null ? void 0 : a.userId) ?? "UNAUTHENTICATED", workspaceId: (a == null ? void 0 : a.workspaceId) ?? "UNAUTHENTICATED", recordingType: l.type, isAddingSteps: l.isAddingStepsInBetween, isRedactorEnabled: u.mode !== "none" }), nt.setContext("settings", { ...c }), nt.setContext("demo", { ...l }), nt.setContext("redactor", { ...u }); const f = new Error(o); f.name = s, nt.captureException(f) }).catch(a => { console.error("Error while getting data for Sentry", a) }); else { const a = So(o) ? o : { extraData: o }, c = bn(a, ["slug"]); nt.addBreadcrumb({ data: c, message: s, level: r, category: n.options.defaults.tag }) } } }) }External destination receiving the log batch
- s1270069.eu-nbg-2.betterstackdata.com
BetterStack log-ingestion endpoint that receives the batched recording logs, including tab URL and account workspace context.
Where it sends data
Destinations our analysis observed Supademo contacting. Sending data somewhere is not a finding on its own - an extension that syncs your settings has to talk to its own server - but it is where your data can go, and who else it goes to.
- ca.media.supademo.com
Supademo sends data to ca.media.supademo.com. No other extension we have analyzed sends data here.
- eu.media.supademo.com
Supademo sends data to eu.media.supademo.com. No other extension we have analyzed sends data here.
- media.supademo.com
Supademo sends data to media.supademo.com. No other extension we have analyzed sends data here.
- s1270069.eu-nbg-2.betterstackdata.com (BetterStack)
Supademo sends data to s1270069.eu-nbg-2.betterstackdata.com (BetterStack). Named as a recipient in this extension's own analysis.