Is Browse AI: Fast Web Scraping & Monitoring safe?
This extension helps users create no-code web scraping and monitoring automations from websites.
It provides a point-and-click workflow for training robots to extract web data, download results as spreadsheets, sync with Google Sheets, and monitor pages for changes on a schedule. The service can also send data to other tools through Zapier, REST API, and webhooks, and it collects anonymous usage data that can be disabled in Settings.
AI-generated. Findings may contain errors. Those marked Verified have been manually reviewed.
Publishers can request a review.
Findings
Browse AI sends recording cookies to its API
An authenticated Browse AI session can collect cookies for the recorded site and domains requested in the recorder tab, then pass them into CreateRobot's GraphQL mutation.
Unauthenticated analysis triggered no cookie or API calls.
You start a Browse AI recording session after signing in.
The cookie-reading branch requires recording permissions and the session-cookie recording option.
The extension reads cookies for the recorded site and request origins, then includes them when the robot is saved.
Unauthenticated test runs did not reach this path, which matches the login and recording preconditions in the code.
| Field | Value | Why it matters | |
|---|---|---|---|
Cookie name | sessionid (illustrative) | This identifies which site cookie is being copied from your browser. | |
Cookie value | 7f4a9c2e51b06d8a (illustrative) | This can carry signed-in session state or other account-specific data for the recorded site. | |
Cookie domain | .example.com (illustrative) | This ties the copied cookie to the website or subdomain where it applies. | |
Cookie path | /account (illustrative) | This narrows where the copied cookie is valid within the site. | |
Expiration | 2026-08-15T12:00:00Z (illustrative) | This shows whether the copied cookie may remain useful beyond the current browser session. |
Service-worker paths that gather cookies and submit them with CreateRobot
const getDomainCookies = ({
domain,
name,
exclude = []
}) => new Promise(
(resolve) => chrome.cookies.getAll({ domain, name }, (cookies) => {
resolve(
cookies.filter((cookie) => cookie.expirationDate && cookie.expirationDate > 0).filter(
(cookie) => exclude.findIndex(
(excludedCookie) => excludedCookie.domain === cookie.domain && excludedCookie.name === cookie.name
) === -1
)
);
})
);const startRecording = (params) => new Promise(async (resolve, reject) => {
const {
url,
rerecording = false,
shouldRecordSessionCookies,
teamName
} = params;
void log$4("info", "Start recording", { rerecording });
if (!url) {
reject(new Error("Origin URL cannot be blank."));
return;
}
if (getEnv$1() === "dev") {
await resetRecordingState(params);
const windowName = 999888777;
window.open(
"about:blank",
windowName.toString(),
["menubar=0"].join(",")
);
await (rerecording ? playingBackState : recordingState$1).set(true);
await recorderTabState$3.set({ id: windowName, url, teamName });
resolve();
} else {
const grantedPermissions = await allPermissionsGranted();
await resetRecordingState(params);
const recorderTab = await recorderTabState$3.get();
if (recorderTab) {
reject(
new Error(
"Cannot start recording a new robot. There is already a recording in progress."
)
);
return;
}
if (!grantedPermissions) {
reject(new Error("Recording permissions were not granted."));
return;
}
if (shouldRecordSessionCookies) {
const origin = getURLOrigin(url);
if (origin) {
const originCookies = await getDomainCookies({
domain: origin.domain
});
await recordedCookiesState$1.set(originCookies);
}
} else {
const incognito = await hasIncognitoAccess();
if (!incognito) {
reject(
new Error(
`Incognito access is required for recording from a blank slate.`
)
);
return;
}
}
const {
window: window2,
createData,
windowCreateSecondParam,
availableScreenWidth,
availableScreenHeight
} = await createBlankPopUpWindow({
maxHeight: params.maxHeight,
maxWidth: params.maxWidth,
incognito: !shouldRecordSessionCookies
});
if (!window2 || !window2.tabs || !window2.tabs[0] || !window2.tabs[0].id) {
void log$4(
"error",
"startRecording: New tab was not created or does not have an ID.",
{
window: window2,
createData,
windowCreateSecondParam,
availableScreenWidth,
availableScreenHeight,
runtimeLastError: chrome.runtime.lastError
}
);
reject(
new Error(
`startRecording: New tab was not created or does not have an ID.`
)
);
return;
}
await (rerecording ? playingBackState : recordingState$1).set(true);
await recorderTabState$3.set({ id: window2.tabs[0].id, url, teamName });
resolve();
}
});const recordedCookiesState = createPersistedState(
"recordedCookies",
[]
);
const recordCookiesBeforeRequest = async (recorderTab) => {
const recordedCookies = await recordedCookiesState.get();
const onBeforeSendHeadersHandler = (details) => {
if (details.url) {
const origin = getURLOrigin(details.url);
if (origin) {
void getDomainCookies({
domain: origin.domain,
exclude: recordedCookies
}).then(async (cookies) => {
recordedCookies.push(...cookies);
await recordedCookiesState.set(recordedCookies);
});
}
}
};
if (recorderTab) {
chrome.webRequest.onBeforeSendHeaders.addListener(
onBeforeSendHeadersHandler,
{ tabId: recorderTab.id, urls: ["http://*/*", "https://*/*"] }
);
}
const cleanUp = () => new Promise((resolve) => {
chrome.permissions.getAll(
({ permissions }) => {
if (permissions && permissions.includes("webRequest") && chrome.webRequest.onBeforeSendHeaders.hasListener(
onBeforeSendHeadersHandler
)) {
chrome.webRequest.onBeforeSendHeaders.removeListener(
onBeforeSendHeadersHandler
);
}
void recordedCookiesState.set([]).then(resolve);
}
);
});
return cleanUp;
};async function recordCookiesOfOriginUrl({
originUrl
}) {
const granted = await chrome.permissions.request({
permissions: ["cookies"],
origins: ["http://*/*", "https://*/*"]
});
if (!granted) {
throw new Error("Recording permissions were not granted.");
}
return new Promise(async (resolve) => {
const cookies = {};
const rawCookies = [];
const newOpenedWindow = await chrome.windows.create({
url: originUrl,
type: "popup",
width: 1,
height: 1,
focused: false,
top: 0,
left: 0
});
if (!newOpenedWindow || !newOpenedWindow.id || !newOpenedWindow.tabs) {
throw new Error("Failed to open a new window");
}
const newTabId = newOpenedWindow.tabs[0].id;
const newWindowId = newOpenedWindow.id;
await recorderTabState$2.set({
id: newWindowId,
url: originUrl,
teamName: "none"
});
function addCookie(domainCookies) {
rawCookies.push(...domainCookies);
domainCookies.forEach((cookie) => {
cookies[cookie.domain] = cookies[cookie.domain] || {};
cookies[cookie.domain][cookie.path] = cookies[cookie.domain][cookie.path] || {};
cookies[cookie.domain][cookie.path][cookie.name] = cookie;
});
}
function grabCookieFromOriginUrl(req) {
const { url } = req;
const domain = new URL(url).hostname;
void chrome.cookies.getAll({ domain }).then((domainCookies) => {
addCookie(domainCookies);
});
}
chrome.webRequest.onBeforeRequest.addListener(grabCookieFromOriginUrl, {
windowId: newOpenedWindow.id,
urls: ["<all_urls>"]
});
chrome.tabs.onUpdated.addListener(async (tabId, changeInfo) => {
if (tabId === newTabId && changeInfo.status === "complete") {
chrome.webRequest.onBeforeRequest.removeListener(
grabCookieFromOriginUrl
);
const url = new URL(originUrl);
const fullHostname = url.hostname;
const parts = fullHostname.split(".");
const baseDomain = parts.slice(-2).join(".");
if (fullHostname) {
const fullHostnameCookies = await chrome.cookies.getAll({
domain: fullHostname
});
addCookie(fullHostnameCookies);
}
if (baseDomain && baseDomain !== fullHostname) {
const baseDomainCookies = await chrome.cookies.getAll({
domain: baseDomain
});
addCookie(baseDomainCookies);
}
void chrome.windows.remove(newWindowId);
resolve(normalizeCookies(cookies));
}
});
});
}const createRobot = async (variables) => {
const result = await getAPIClient().mutate({
mutation: gql`
mutation CreateRobot(
$chromeVersion: String!
$extensionVersion: String!
$purpose: RobotPurpose!
$cookies: JSON!
$recordedSessionCookies: Boolean!
$teamId: String!
) {
createRobot(
chromeVersion: $chromeVersion
extensionVersion: $extensionVersion
purpose: $purpose
cookies: $cookies
recordedSessionCookies: $recordedSessionCookies
teamId: $teamId
) {
id
uploadingSteps {
id
rawStepsURL
}
}
}
`,
variables: {
...variables,
chromeVersion: getBrowserVersion() || "",
extensionVersion: getExtensionVersion()
}
});
const robotId = result.data.createRobot.id;
const robotVersionId = result.data.createRobot.uploadingSteps.id;
const rawStepsURL = result.data.createRobot.uploadingSteps.rawStepsURL;
await createdRobotState$1.set({
robotId,
robotVersionId,
rawStepsURL
});
await finishRobotRecording({
robotId,
robotVersionId,
rawStepsURL
});
await createdRobotState$1.set(null);
return { robotId, robotVersionId, rawStepsURL };
};- internal-api.browse.ai
Browse AI GraphQL API endpoint used when the extension saves a recorded robot with cookie JSON.
Browse AI uploads recorded page and DOM step details
Browse AI recording captures interaction steps: page URL, title, DOM targets, page metadata, viewport size, scroll position, user agent.
The worker uploads steps via Browse AI's GraphQL API.
You record a Browse AI workflow on a web page.
The recording feature listens for page events while the recorder is active.
The extension stores detailed interaction steps and uploads them for the robot workflow.
Those steps include page context and DOM selectors tied to the recorded interaction.
| Field | Value | Why it matters | |
|---|---|---|---|
Page URL | https://app.example.com/orders/1042?status=pending | Shows the exact page where the recorded action happened. | |
Page title and metadata | Pending Orders - Example Admin | Adds readable page context and selected metadata from the page. | |
DOM selectors | button[data-testid="approve-order"] | Identifies the page elements involved in your recorded clicks and inputs. | |
Viewport and scroll position | 1366x768, scrollTop 420 | Shows your browser viewport size and where the page was scrolled during the step. | |
User agent | Mozilla/5.0 (Windows NT 10.0; Win64; x64) Chrome/124.0.0.0 | Can identify browser and operating-system details when the recorder includes it. |
The content script builds step records with page and viewport context
const wr = function() {
for (var e = [], t = document.getElementsByTagName("meta"), n = document.getElementsByTagName("link"), r = 0; r < t.length; r++) {
var i = t[r].getAttribute("name"),
o = t[r].getAttribute("property"),
a = t[r].getAttribute("content");
a && ("msapplication-TileColor" === i || "msapplication-TileImage" === i || "application-name" === i || "description" === i || "robots" === i ? e.push({
itemType: "meta",
name: i,
content: a
}) : "og:site_name" !== o && "og:url" !== o && "og:image" !== o || e.push({
itemType: "meta",
property: o,
content: a
}))
}
for (var s = 0; s < n.length; s++) {
var l = n[s].getAttribute("rel"),
c = n[s].getAttribute("href"),
u = n[s].getAttribute("sizes"),
p = n[s].getAttribute("color"),
d = n[s].getAttribute("title"),
h = n[s].getAttribute("type"),
m = n[s].getAttribute("crossOrigin");
!c || "apple-touch-icon-precomposed" !== l && "icon" !== l && "shortcut icon" !== l && "mask-icon" !== l && "fluid-icon" !== l && "manifest" !== l && "canonical" !== l || e.push(yr(yr(yr(yr(yr({
itemType: "link",
rel: l,
href: c
}, u ? {
sizes: u
} : null), p ? {
color: p
} : null), d ? {
title: d
} : null), h ? {
type: h
} : null), m ? {
crossOrigin: m
} : null))
}
return e
}; const Tr = function(e, t) {
var n = e.event,
r = e.targetSelectors,
i = void 0 === r ? [] : r,
o = e.containerSelectors,
a = e.tabId,
s = e.skipIfTargetNotFound,
l = e.targetId,
c = arguments.length > 2 && void 0 !== arguments[2] && arguments[2];
return kr(kr({
targetId: l,
event: n,
targetSelectors: i
}, void 0 !== o ? {
containerSelectors: o
} : {}), {}, {
time: Date.now(),
page: kr({
tabId: a,
url: window.location.href,
title: document.title,
meta: wr()
}, t),
emulationOptions: {
viewport: {
width: Math.max(document.documentElement.clientWidth, window.innerWidth),
height: Math.max(document.documentElement.clientHeight, window.innerHeight),
deviceScaleFactor: Math.round(100 * window.devicePixelRatio) / 100
},
scrollLeft: window.pageXOffset || document.documentElement.scrollLeft,
scrollTop: window.pageYOffset || document.documentElement.scrollTop,
userAgent: c ? navigator.userAgent : null
},
skipIfTargetNotFound: s
})
}; for (;;) switch (e.prev = e.next) {
case 0:
return e.next = 2, gi.get();
case 2:
if (e.sent) {
e.next = 6;
break
}
return Di(), e.abrupt("return");
case 6:
return xi.push(t), e.next = 9, _i.set(xi);
case 9:
if (!pi) {
e.next = 12;
break
}
return e.next = 12, pi({
steps: [].concat(xi)
});
case 12:
case "end":
return e.stop()
}
}), e)
})));
return function(t) {
return e.apply(this, arguments)
}
}(), }(), Mi || (Mi = !0, bi.forEach((function(e) {
Hr(e) ? window.addEventListener(e, Ri, {
capture: !0,
once: "load" === e
}) : document.addEventListener(e, Ri, {
capture: !0
})
}))), _i.get().then(function() {The service worker uploads the recorded step list
const uploadRobotSteps = ({
createdRobot: { robotVersionId, rawStepsURL },
steps
}) => {
return fetch(rawStepsURL, {
method: "PUT",
headers: { "content-type": "application/json" },
body: JSON.stringify(steps)
}).then((res) => {
if (res.status >= 400) {
throw new Error(res.statusText);
}
return res;
}).then(
() => getAPIClient().mutate({
mutation: gql`
mutation uploadedRobotSteps($robotVersionId: String!) {
uploadedRobotSteps(robotVersionId: $robotVersionId) {
id
}
}
`,
variables: { robotVersionId }
}).then((result) => {
const robotVersionId2 = result.data.uploadedRobotSteps.id;
return Boolean(robotVersionId2);
})
);
};const finishRobotRecording = async ({
robotVersionId,
rawStepsURL,
robotId
}) => {
const teamName = (await recorderTabState$5.get())?.teamName || void 0;
const steps = await stepsState$2.get();
const reloadSteps = await reloadStepsState$1.get();
const mergedSteps = mergeStepsByTime(steps, reloadSteps);
if (steps) {
void log$6(
"info",
"Robot was created. Reset 'steps' and begin uploading the recording...",
{}
);
await uploadRobotSteps({
createdRobot: { robotVersionId, rawStepsURL },
steps: mergedSteps
});
void log$6("info", "Robot recording upload was successful.");- internal-api.browse.ai
Browse AI GraphQL API receives the upload-complete mutation for recorded robot steps.
Page URLs and referrers sent to Datadog during recording
Starting a Browse AI recording flow adds the current page URL, referrer, host, path, and scheme to its logging context, forwarded through the background logger to Datadog when anonymous data sharing stays enabled, the extension default.
You start a Browse AI recording flow.
The popup code can launch recording for the current tab when a recording purpose is selected.
The extension adds page URL and referrer fields to its logging context.
The background logger can forward that context to Datadog while anonymous data sharing is enabled.
| Field | Value | Why it matters | |
|---|---|---|---|
Current page URL | https://app.example.com/projects/acme-dashboard?view=recording | Shows the exact page open when the recording-related log event is created. | |
Referring page | https://app.example.com/projects | Shows the page that led you to the current page when the browser exposes a referrer. | |
Site host | app.example.com | Identifies the website where the recording-related event happened. | |
Path and scheme | /projects/acme-dashboard and https | Adds the page path and whether the page used HTTP or HTTPS. |
The content script adds page context before sending a logger message
const shareAnonymousDataState$1 = createPersistedState("shareAnonymousData", true);
const Log = (path) => {
if (getEnv$1() === "content") {
return async (type, description, context) => {
if (path !== "recorder/src/helpers/callRemoteFunction.ts" && !inIframe()) {
const modifiedContext = {
...context,
view: {
referrer: document.referrer,
url: document.URL,
url_details: {
host: document.location?.host,
path: document.location?.pathname,
scheme: (document.location?.protocol || "").replace(/:$/, "")
}
}
};
return callRemoteFunction({
to: "background",
name: "logger",
params: [
{
type,
description,
context: modifiedContext,
path,
subPackageName: "content"
}
]
});
} else {
try {
if (localStorage.getItem("DEBUG_LEVEL") === "log") {
console[type](path, description, context);
}
} catch {
}
return false;
}
};
} else {
return async (type, description, context) => {
const shareAnonymousData = await shareAnonymousDataState$1.get();
return Log$1(path, { uploadToDatadog: shareAnonymousData })(
type,
description,
context
);
};
}
};
const logger = ({
type,
description,
context,
path,
subPackageName
}) => {
const log = Log$1(path, { subPackageNameOverwrite: subPackageName });
return log(type, description, context);
};The background logger initializes and forwards to Datadog
var define_process_env_default$5 = { CHROME_EXTENSION_ENV: "background", REACT_APP_MOCK_ACTIVATION: "false", REACT_APP_API_URL: "https://internal-api.browse.ai", REACT_APP_EXTENSION_LOGIN_URL: "https://dashboard.browse.ai/extension-activate", REACT_APP_MARKETING_SITE_URL: "https://browse.ai/", REACT_APP_DASHBOARD_URL: "https://dashboard.browse.ai", REACT_APP_HELP_URL: "https://browse.ai/support", REACT_APP_DEBUG_LEVEL: "error", REACT_APP_PACKAGE_NAME: "recorder", REACT_APP_DATADOG_CLIENT_TOKEN: "pub829657516887adb6c583bd9646064850" };
const clientToken = define_process_env_default$5.DATADOG_CLIENT_TOKEN || define_process_env_default$5.REACT_APP_DATADOG_CLIENT_TOKEN;
if (clientToken && getExtensionEnv() !== "content") {
datadogLogs.init({
clientToken,
datacenter: "us",
forwardErrorsToLogs: true,
sampleRate: 100
}); if (!context.user && !defaultLogContext.user) {
const user = await getUser(subPackageName);
if (user) {
setDefaultLogContext({
user
});
context.user = user;
}
}
datadogLogs.logger[type](description, {
custom: {
packageName,
subPackageName,
path,
humanReadableDate: date.format(dateHumanReadableFormat),
timestamp: date.valueOf(),
context: {
...defaultLogContext,
...context
}
},
stage: define_process_env_default$5.STAGE || define_process_env_default$5.REACT_APP_STAGE,
source: "browser",
service: `${packageName}${subPackageName !== packageName ? `/${subPackageName}` : ""}`
});
}
return true;- browser-http-intake.logs.datadoghq.com
Datadog browser log intake receives extension log events that include the page context added by the content script.