A UI prototype can pass its demo and still send an email address to a vendor the team never approved. By the time that request appears in a security review, changing the library may mean changing the design and the estimate. I would give privacy failures veto power during the first runnable spike, even if that delays the quote, because disclosure cannot be undone with a later refactor.
A browser request can create a privacy obligation before the feature exists
Imagine you are building a sign-in page. The design includes a help widget, error reporting, and a UI library that loads a font from its own CDN. Nothing looks sensitive in the mock-up. In the browser, though, the page may send its URL, IP address, referrer, and error context to separate providers. If the URL contains an email address or an invitation token, those requests can disclose it before the user submits the form.
This is why I would not put live customer accounts into a prototype: test accounts can exercise the same navigation and error paths without exposing real people to vendors whose processing terms have not been checked. Use invented names, invented domains, and tokens that cannot authenticate against production. A staging environment is not automatically private, because its browser still makes real network requests and its monitoring tools may still retain events.
Your first task is to draw a small data-flow map, not a polished architecture diagram. Write down what the browser knows on initial load, what it learns after sign-in, and which destinations receive requests at each point. Distinguish the app’s own origin from an identity provider, error tracker, analytics service, CDN, and help widget. That distinction matters because a same-looking script can run with access to page data while reporting to an organisation with a different retention policy.
For a hypothetical sign-in spike, use 20 invented accounts as test input; that is enough to try different error and success paths without treating a handful of clicks as exhaustive evidence. Set zero unapproved third-party requests before sign-in as a proposed acceptance threshold, then ask your lead to approve or change it. The number is a gate to tune, not a claim that every external request is unsafe: an approved identity-provider redirect may be essential, while a help widget on the first page may be unnecessary.
Read Before You Scope It, Test the Stack’s Hidden Costs before you finalize the estimate, because a cost check is useful but cannot recall an identifier already sent to a third party. Record each unexpected destination with the exact field or URL it receives. “Uses analytics” is too vague to support a decision; “sends the invitation token in the page URL to the analytics host on initial load” gives the team something it can fix.
A passing UI demo does not prove that the browser stays within the boundary
Run the proposed page locally and watch requests while you navigate, submit invalid input, and trigger an error. Chrome DevTools’ Network panel is a quick manual starting point, while Playwright can make one narrow check repeatable in a pull request. The script below reports cross-origin requests observed during initial load; it does not label them malicious, and it does not inspect what happens after interaction.
Install Playwright with npm install -D playwright, install its browser with npx playwright install chromium, save this as audit.mjs, and run it with TARGET_URL=http://localhost:3000 node audit.mjs while your app is running.
import { chromium } from 'playwright';
const target = process.env.TARGET_URL || 'http://localhost:3000';
const origin = new URL(target).origin;
const browser = await chromium.launch();
const page = await browser.newPage();
const external = new Set();
page.on('request', request => {
if (new URL(request.url()).origin !== origin) external.add(request.url());
});
await page.goto(target);
await page.waitForTimeout(1500);
console.log([...external].join('\n') || 'No cross-origin requests observed');
await browser.close();
The 1,500-millisecond wait is an adjustable observation window, not a guarantee that delayed requests will appear; extend it or drive the relevant interaction when the app loads services later. Also inspect request bodies and redirect chains in DevTools, because a destination list alone cannot tell you whether a field contains an identifier. If you use mitmproxy for a fuller trace, keep the trace out of a shared ticket until you have checked it for tokens.
Check the response’s Content-Security-Policy, or CSP, alongside that trace. A restrictive connect-src can make an unplanned analytics endpoint fail visibly, whereas a broad wildcard may let it operate without drawing attention; neither setting replaces reviewing the data sent to an allowed host. Inspect script-src for third-party scripts and use Subresource Integrity, or SRI, for eligible static CDN assets, because pinning the expected file can limit unexpected changes to that asset. SRI does not solve privacy concerns when the expected script itself collects data.
Read Test the UI Framework Before You Estimate the Build before you choose a UI dependency, because rendering a component successfully says little about the requests its documentation examples or optional plugins make. Test the components and configuration you actually plan to ship. A framework should not get blamed for a sample app’s analytics plugin, and the plugin should not get a free pass because the framework renders well.
Hosted sign-in and self-hosted sign-in move the boundary in different directions
For authentication, compare a hosted OpenID Connect sign-in redirect with a self-hosted Keycloak deployment. Hosted sign-in wins when your team needs the provider to operate the identity service and can approve its handling of account identifiers, IP addresses, logs, and support access. Its cost is an additional party in the identity data flow, plus contractual and configuration work. Self-hosted Keycloak wins when keeping identity events inside your controlled infrastructure is a firm requirement and the team can operate it reliably. Its cost is patching, backups, monitoring, incident response, and someone being available when sign-in breaks.
Neither option is “private by default,” because your configuration determines what data reaches it and how long records persist. With hosted sign-in, test the redirect URL and callback for identifiers you did not mean to place there. With Keycloak, check whether access logs, database backups, and error reports contain identifiers, since owning the server also means owning those copies. Ask who can delete an account’s records and demonstrate the answer in the spike rather than accepting “we can add deletion later.”
For a browser-based app, a suitable redirect design uses OAuth 2.0 Authorization Code with Proof Key for Code Exchange (PKCE, RFC 7636) and validates OpenID Connect state and nonce where applicable. Those mechanisms address particular authorization and response-substitution risks; they do not decide whether the identity provider may retain a user’s email address. If your app uses server-managed sessions after the callback, examine its cookie settings, including Secure, HttpOnly, and SameSite, because the choice affects transport, script access, and cross-site sending. Do not copy a cookie setting from an example without testing the redirect flow it must support.
I would not store session credentials in localStorage simply to avoid designing the callback and cookie flow, because page scripts can read that storage if an injection vulnerability occurs. A server-managed session does not eliminate cross-site request forgery or server compromise, so its advantage depends on appropriate cookie settings and request protections. The useful question for an estimate is not “which auth library is fastest to install?” but “which party sees which identity fields, and who will operate the protections we chose?”
The approval decision needs evidence, not a dependency score
Supply-chain scanners help, but they answer a narrower question than a privacy review. npm audit and Semgrep can surface known package issues or suspicious code patterns; neither can approve a vendor’s retention policy by looking at package files. OWASP ZAP can probe a running app for certain web vulnerabilities, but it will not decide whether sending a search term to an analytics provider fits your product’s promises. Run the tools, then attach their findings to the data-flow map instead of treating a clean report as permission to ship.
Error monitoring deserves its own test. In Sentry, review how your SDK configuration handles request bodies, user context, and breadcrumbs before deliberately throwing an error with synthetic input. OpenTelemetry spans also need scrutiny when you add attributes, because a convenient user identifier in a span can spread through exporters and storage systems. For the spike, propose 14 days as a provisional retention window for diagnostic events, then have the owner tune it to the operational need and the provider’s available controls. A retention setting is only meaningful if someone verifies where copies and backups go.
Set aside 2 hours as an initial investigation budget, not as a promise that the work is finished: use it to capture the initial-load trace, one sign-in path, and one failure path. If those paths reveal an unknown recipient of personal data, stop estimating that integration as though it were settled. Ask for a decision to remove the recipient, change what is sent, or approve its use with the appropriate owner. That pause is defensible because discovering the same issue after release turns a design choice into a disclosure investigation.
Make the evidence easy for a senior developer to review: note the route and action, the receiving host, the field observed, the tool used to observe it, and the proposed change. Keep raw traces access-controlled if they contain tokens, even synthetic ones that still work in staging. Record what you did not test, such as mobile browsers or delayed requests, because a precise boundary around the evidence is more useful than an unsupported “privacy checked” label.
Start with the sign-in page your team can run today. Open its Network panel, load it before authenticating, and write down every destination outside your app’s origin. Then repeat with invented credentials and one deliberate error. Bring one unexpected request—not a general warning—to the next estimate discussion, with the field it carries and a proposed way to remove or approve it.


