Is AI Chat Exporter: Gemini to PDF, MD and more safe?
AI Chat Exporter sends the full text of exported Gemini conversations, plus your Google account name and email, to the vendor's servers.
When you export a Gemini chat, the extension packs the entire conversation (every message turn) along with the account holder's name and email and POSTs it to the vendor's backend at api.ai-chat-exporter.com to render the PDF server-side. The same inbound message, including the conversation body and your name/email, is also attached as a property on a PostHog analytics event and forwarded through the vendor's analytics proxy at phus.ai-chat-exporter.com. The extension only runs on gemini.google.com, but the conversation content leaves your browser whenever you trigger an export.
AI-generated. Findings may contain errors. Those marked Verified have been manually reviewed.
Publishers can request a review.
Findings
Feedback form attaches your entire local storage without disclosure
Submitting the Feedback dialog makes the background read every local-storage key and attach it to the vendor report, including your account name, email, and internal IDs, even with email left blank.
A planted storage marker confirmed this.
You open the extension's Feedback dialog, type a short description, and leave the optional email field blank before clicking Submit.
This is the only user-facing form field for a name or email in the feedback flow.
The background script reads every key in the extension's local storage and attaches the whole dump to the report before sending it to the vendor's backend.
This includes your signed-in account's name and email and internal account/analytics identifiers, none of which appear anywhere in the visible feedback form.
| Field | Value | Why it matters | |
|---|---|---|---|
Your typed feedback message | The PDF export sometimes fails on long conversations. (illustrative) | The description you write in the dialog is sent as part of the report, as you'd expect. | |
Signed-in account name and email | Jordan Lee <jordan.lee@example.com> (illustrative) | Even with the visible email field left blank, your account's name and email are pulled from local storage and attached anyway. | |
Account and analytics client IDs | cusId: u_8f2c1a90, analytics client ID: 1823456789.1700000000 (illustrative) | A persistent account ID and analytics client ID travel with the report, letting it be linked to your other activity with this extension. | |
Everything else cached by the extension | cached account-info and usage records (illustrative) | Any other data the extension has stored locally is included wholesale, whether or not it has anything to do with your feedback. |
| Content-Type | application/json |
{"type":"bug","description":"The PDF export sometimes fails on long conversations.","email":"","screenshotUrls":[],"context":{},"errorInfo":null,"metaData":{},"ext":"AI Chat Exporter","cid":"1823456789.1700000000","version":"4.0.0","storage":{"session":{"user":{"cusId":"u_8f2c1a90","given_name":"Jordan","family_name":"Lee","email":"jordan.lee@example.com"}},"ga.clientid":"1823456789.1700000000"}} (illustrative reconstruction of the captured request shape)The feedback handler reads all of local storage and attaches it to the request body
function St() {
return (St = o(a().mark((function e(t, r, n) {
var o, s, i, c, l, u;
return a().wrap((function(e) {
for (;;) switch (e.prev = e.next) {
case 0:
return o = { errcode: -1, errmsg: "unknown error", rslt: null }, e.prev = 1, e.next = 4, Ue();
case 4:
return s = e.sent, i = chrome.runtime.getManifest().version, e.next = 8, chrome.storage.local.get(null);
case 8:
return c = e.sent, l = Ot(Ot({}, t.data), {}, {
ext: d.ext.name,
cid: s,
version: i,
storage: c
}), v.debug("fetch feedback API, body keys:", Object.keys(l)), e.next = 13, fetch(d.backend.feedback, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(l)
});
case 13:
if ((u = e.sent).ok) { e.next = 18; break }
o = { errcode: -2, errmsg: "API error: ".concat(u.status, " ").concat(u.statusText), rslt: null }, e.next = 21;
break;
case 18:
return e.next = 20, u.json();
case 20:
o = e.sent;
case 21:
e.next = 27;
break;
case 23:
e.prev = 23, e.t0 = e.catch(1), v.error("handleOnMsgFeedback error:", e.t0), o = { errcode: -3, errmsg: e.t0.message, rslt: null };
case 27:
v.debug("onMessage[feedback] sendRsp errcode:", o.errcode), n(o), Xe("Background Runtime Message", {
extname: d.ext.name,
status: "end",
cmd: "feedback",
rsp: o
});
case 30:
case "end":
return e.stop()
}
}), e, null, [[1, 23]])
}))).apply(this, arguments)
}chrome.runtime.onMessage.addListener((function(e, t, r) {
v.debug("in handleRuntimeOnMessage, req:", e), v.debug("in handleRuntimeOnMessage, sender:", t), Xe("Background Runtime Message", {
status: "begin",
cmd: e.cmd,
req: e
});
var n = null;
if ("open_options_page" === e.cmd) chrome.runtime.openOptionsPage(), r({ errcode: 0, errmsg: "options page opened" });
else {
if ("pdf" === e.cmd) return function(e, t, r) { ut.apply(this, arguments) }(e, t, r), !0;
if ("account" === e.cmd) return ft(e, t, r), !0;
if ("usage" === e.cmd) return yt(e, t, r), !0;
if ("atft" === e.cmd) return wt(e, t, r), !0;
if ("feedback" === e.cmd) return function(e, t, r) { St.apply(this, arguments) }(e, t, r), !0;
if ("image_fetch" === e.cmd || "fetch_and_upload_image" === e.cmd) return function(e, t, r) { Tt.apply(this, arguments) }(e, t, r), !0;
v.error("wrong req.msg"), r(n = { errcode: -1, errmsg: "wrong req.msg" })
}
v.debug("rsp:", n)
})), chrome.runtime.onMessageExternal.addListener(Ct)- apir.ai-chat-exporter.com
The extension vendor's own API backend; receives the feedback report including the full local-storage attachment.