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.
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.
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.
Log entries are held in an in-memory queue and sent in batches about every five seconds when there is something to send.
| Field | Value | Why it matters | |
|---|---|---|---|
Recorded tab URL | https://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 account | userId: user_2N7fK9aQ, name: Jane Doe (illustrative) | Links the log entry to the Supademo account used during the recording session. | |
Workspace context | workspaceId: ws_8c2fb1e4 (illustrative) | Connects the recording activity to the workspace stored by the extension. | |
Browser tab list | id: 123, isActive: true, windowId: 7 (illustrative) | Adds browser context about open tabs, including which tab is active and the window it belongs to. | |
Runtime environment | browser: Chrome, os: Mac, env: production, version: 7.4.8 (illustrative) | Adds browser, operating system, environment, and extension version details to the same log entry. |
| Content-Type | application/json |
| Authorization | Bearer <redacted> |
The logger queues account and tab context, then posts the batch to BetterStack
async 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
})
}
}
})
}async 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
})
}
}
})
}- s1270069.eu-nbg-2.betterstackdata.com
BetterStack log-ingestion endpoint that receives the batched recording logs, including tab URL and account workspace context.