Is Writesonic - SEO Keywords & AI Search Insights safe?
Writesonic is medium risk. The extension sends writing-feature interaction events to Google Analytics and Mixpanel with persistent analytics IDs; sign-up/upgrade events include a current_url value. Not triggered in our unauthenticated session; seen in shipped code.…
Who publishes itWritesonic, Inc. - 1 other listing from the same operator, none carrying a finding
Writesonic, Inc. - 1 other listing from the same operator, none carrying a finding
What this publisher told the store about itself, and the other listings that told it the same thing.
Same store account
1 other listing published from this account, 1k+ users between them, none of them carrying a finding.
Shared hosts - 1 hostname
Hostnames hardcoded in this extension that few other listings call. That can mean one operator behind both, and it can equally mean a small shared vendor, so it is context rather than a conclusion. Hosts that many listings call are left out: they are services, not connections.
AI-generated. Findings may contain errors. Those marked Verified have been manually reviewed.
Publishers can request a review.
Findings
Writing actions send analytics identifiers
The extension sends writing-feature interaction events to Google Analytics and Mixpanel with persistent analytics IDs; sign-up/upgrade events include a current_url value.
Not triggered in our unauthenticated session; seen in shipped code.
- Severity
- Medium unwanted
- Type
- Unexpected
- CWE
- CWE-200
- Source
- Dynamic sandbox
You use a supported Writesonic writing feature or account-action control.
The code includes handlers for Gmail, Twitter, LinkedIn, sign-up, and upgrade interactions.
The extension turns the click into an analytics event and sends it to two analytics services.
Some account-action events include a current_url value, and Mixpanel events include a distinct identifier.
- Interaction nameCS_CE_Gmail_Write_Button_Click
Shows which extension feature you used, such as a Gmail write click or LinkedIn message click.
- Current URLhttps://app.writesonic.com/settings/billing?utm_source=chatsonic-chrome-extension
For sign-up and upgrade events, this connects the analytics event to the page or destination URL involved in the action.
- Google Analytics client ID8b0f7d14-35a6-44e0-9e47-d48a4b6fd7d2 (illustrative)
Lets Google Analytics connect multiple extension events to the same browser profile.
- Mixpanel distinct IDuser_742918 (illustrative)
Can identify a signed-in Writesonic account or an anonymous extension profile across events.
- Tone settingProfessional
Adds feature-specific context about how you used the writing tool.
The code posts a JSON body with client_id and events; no successful dynamic request body was recorded for this claim.
The background script posts the Mixpanel event body; no successful extension-originated body was recorded during the unauthenticated dynamic session.
- accept
- text/plain
- content-type
- application/json
Feature clicks are mapped to analytics events and sent to two services
Click handlers call trackAnalytic
js/content_main.jsasync onClickWriteBtn({ text: t, toneOfVoice: e}) { this.parent.typeBtnClick = "write_msg", this.helper.getTextFormElements(), c.trackAnalytic("gmail_write_btn_click", { toneOfVoice: e }), this.parent.args.onClickWriteBtn(t, e)}async onClickEWidgetWriteBtn({ e: t, text: e, toneOfVoice: i}) { const n = t.target.closest("form").querySelector(".msg-form__contenteditable"); if (!n) return void this.services.execEmailWSEndWriting(); this.parent.typeBtnClick = "mail", this.parent.activeTextBox = n, this.helpers.addLoaderMessageBox(this.parent.activeTextBox); const s = { token: c.token, type: "linkedin_message", data: { toneOfVoice: i, msg: e } }; c.trackAnalytic("linkedin_write_msg_btn_click", { toneOfVoice: i }), this.parent.socialIframe.sendMessageToIframeForce("integ_ws_msg", s)}Analytics router and event dispatch
js/content_main.jstrack(t, e = {}) { switch (t) { case "signup": "native" === e.scope ? fc.preSignUpBtnClickSaveStorage(e?.href) : fc.preSignUpBtnClick(e?.href); break; case "upgrade": "native" === e.scope ? fc.upgradeBtnClickSaveStorage(e.email, e.fullName, e.href) : fc.upgradeBtnClick(e.email, e.fullName, e.href); break; case "gmail_write_btn_click": fc.gmailWriteBtnClick(e.toneOfVoice); break; case "gmail_stop_btn_click": fc.gmailStopBtnClick(e.toneOfVoice); break; case "email_summary_genereted": fc.gmailSummaryGenereted(); break; case "email_reply_btn_click": fc.gmailReplyToEmailBtnClick(e.toneOfVoice); break; case "twitter_write_btn_click": fc.twitterWriteBtnClick(e.toneOfVoice); break; case "twitter_stop_btn_click": fc.twitterStopBtnClick(""); break; case "twitter_reload_btn_click": fc.twitterReloadBtnClick(""); break; case "twitter_thread_reply": fc.twitterReplyToThread(); break; case "twitter_thread_summary": fc.twitterThreadSummary(); break; case "linkedin_write_btn_click": fc.linkedinWritePostBtnClick(e.toneOfVoice); break; case "linkedin_write_msg_btn_click": fc.linkedinWriteMsgBtnClick(e.toneOfVoice) }}async exec(t) { await this.sendRequestGA4(t), this.sendRequestMixpanel(t)}Google Analytics and Mixpanel payload construction
js/content_main.jsasync sendRequestGA4(t) { const e = { name: t.name }; if (t.params && (e.params = t.params), !this.gaClientId) { const t = await chrome.storage.local.get(null); if (t.ga_client_id) this.gaClientId = t.ga_client_id; else { const t = W(); this.gaClientId = t; const e = { ga_client_id: t }; await chrome.storage.local.set(e) } } fetch(`https://www.google-analytics.com/mp/collect?measurement_id=${this.measurementId}&credential=<redacted>`, { method: "POST", body: JSON.stringify({ client_id: this.gaClientId, events: [e] }) })}async sendRequestMixpanel(t) { try { let e = {}; const i = t.name; t.params && (e = t.params || e); const n = await this.getMixpanelDistinctID(); try { const t = [{ event: i, properties: { token: mo.mixpanel_cred.project_token, distinct_id: n, $insert_id: W(), time: Date.now(), ...e } }]; xo({ reason: lt.EXEC_MP_TRACK, data: { body: t } }) } catch (t) { console.log(t) } } catch (t) { console.log(t) }}Background Mixpanel network send
js/background.jsasync execRequestToMixpanel(e) { try { const t = JSON.stringify(e); fetch(r.mixpanel_cred.base_url + "/track", { method: "POST", headers: { accept: "text/plain", "content-type": "application/json" }, body: t }) } catch (e) { console.log(e) }}- www.google-analytics.com
Receives Measurement Protocol events with the extension's analytics client ID and event names.
- api.mixpanel.com
Receives Mixpanel track events with distinct_id, event name, insert ID, timestamp, and event-specific fields.
Sign-up analytics include IP address and page URL
Sign-up and upgrade actions can make the extension resolve your public IP via db-ip.com, then attach that IP and a page URL to events sent to Google Analytics and Mixpanel.
Not triggered in our unauthenticated session; seen in shipped code.
- Severity
- Medium unwanted
- Type
- Unexpected
- CWE
- CWE-359
- Source
- Dynamic sandbox
You click a sign-up or upgrade action in the extension flow.
The code routes those actions through named analytics events for sign-up and upgrade clicks.
The extension looks up your public IP address and attaches it to an analytics event with a page URL.
Upgrade events also include the email address and full name values passed into the upgrade handler.
- Your public IP address198.51.100.23 (illustrative)
Identifies the network address seen by websites and services when the event is created.
- Page URLhttps://app.writesonic.com/settings/billing?utm_source=chatsonic-chrome-extension
Shows which page or destination URL was connected to the sign-up or upgrade action.
- Email addressalex.chen@example.com (illustrative)
Can identify the account connected to an upgrade action when that value is passed to the handler.
- Full nameAlex Chen (illustrative)
Can identify the person connected to an upgrade action when that value is passed to the handler.
The code reads the JSON ipAddress field from this response; the unauthenticated dynamic session did not record this request.
The code posts a JSON body with client_id and events; no successful dynamic request body was recorded for this claim.
The shipped code builds sign-up and upgrade analytics with IP lookup
Sign-up and upgrade event builders
js/content_main.jsasync preSignUpBtnClickSaveStorage(t) { const e = { name: "CS_CE_Sign_Up_Button_Click", params: { current_url: t, ipAddress: await this.#ce() || "" } }; await this.#le(e)}async preSignUpBtnClick(t) { const e = { name: "CS_CE_Sign_Up_Button_Click", params: { current_url: t, ipAddress: await this.#ce() || "" } }; await gc.exec(e)}async upgradeBtnClick(t, e, i) { const n = { name: "CS_CE_Upgrade_Button_Click", params: { current_url: i, ipAddress: await this.#ce() || "", email: t, fullName: e } }; gc.exec(n)}async upgradeBtnClickSaveStorage(t, e, i) { const n = { name: "CS_CE_Upgrade_Button_Click", params: { current_url: i, ipAddress: await this.#ce() || "", email: t, fullName: e } }; await this.#le(n)}Public-IP lookup helper
js/content_main.jsasync #ce() { try { const t = await fetch("https://api.db-ip.com/v2/free/self"); return (await t.json()).ipAddress } catch (t) { return null }}Google Analytics and Mixpanel dispatch
js/content_main.jsasync exec(t) { await this.sendRequestGA4(t), this.sendRequestMixpanel(t)}async sendRequestGA4(t) { const e = { name: t.name }; if (t.params && (e.params = t.params), !this.gaClientId) { const t = await chrome.storage.local.get(null); if (t.ga_client_id) this.gaClientId = t.ga_client_id; else { const t = W(); this.gaClientId = t; const e = { ga_client_id: t }; await chrome.storage.local.set(e) } } fetch(`https://www.google-analytics.com/mp/collect?measurement_id=${this.measurementId}&credential=<redacted>`, { method: "POST", body: JSON.stringify({ client_id: this.gaClientId, events: [e] }) })}async sendRequestMixpanel(t) { try { let e = {}; const i = t.name; t.params && (e = t.params || e); const n = await this.getMixpanelDistinctID(); try { const t = [{ event: i, properties: { token: mo.mixpanel_cred.project_token, distinct_id: n, $insert_id: W(), time: Date.now(), ...e } }]; xo({ reason: lt.EXEC_MP_TRACK, data: { body: t } }) } catch (t) { console.log(t) } } catch (t) { console.log(t) }}- api.db-ip.com
Receives the IP lookup request and returns the public IP address used in the analytics event.
- www.google-analytics.com
Receives the Measurement Protocol analytics event.
- api.mixpanel.com
Receives the Mixpanel tracking event relayed by the background script.