For the full Formo security overview, see formo.so/security and the Security documentation.
| Version | Supported |
|---|---|
| 1.x.x | ✅ |
| < 1.0 | ❌ |
If you discover a security vulnerability in @formo/analytics, please report it responsibly.
Do not open a public GitHub issue for security vulnerabilities.
Instead, please email security@formo.so with:
- A description of the vulnerability
- Steps to reproduce
- Potential impact
- Suggested fix (if any)
We will acknowledge receipt within 48 hours and aim to provide a fix within 7 days for critical issues.
All event data is transmitted exclusively over HTTPS/TLS to https://events.formo.so. The API endpoint is hardcoded with the https:// scheme — there is no HTTP fallback and no configuration that downgrades to plaintext transport.
When using a custom apiHost (proxy), it is the integrator's responsibility to ensure the proxy endpoint also uses HTTPS.
When loading the SDK via a <script> tag from a CDN (e.g. unpkg), you should use Subresource Integrity to ensure the script has not been tampered with in transit:
<script
src="https://unpkg.com/@formo/analytics@1.0.0/dist/index.umd.min.js"
integrity="sha384-<HASH>"
crossorigin="anonymous"
></script>SRI hashes for each release are published alongside the package. See the SRI documentation for per-version hashes and verification instructions, or check the GitHub releases page.
Using the npm package (@formo/analytics via npm/pnpm/yarn) avoids CDN delivery risks entirely, since the package is verified by the registry and lockfile.
The SDK is compatible with strict Content Security Policies. The minimum required directives are:
script-src 'self';
connect-src 'self' https://events.formo.so;
If using a custom apiHost (first-party proxy), add your proxy domain to connect-src:
connect-src 'self' https://events.formo.so https://your-proxy.example.com;
If loading via CDN, add the CDN origin to script-src:
script-src 'self' https://unpkg.com;
The SDK does not require unsafe-eval or unsafe-inline. It uses no eval(), innerHTML, document.write(), or dynamic script injection. All network communication uses the standard fetch() API.
See the CSP documentation for detailed configuration guidance.
The SDK can optionally wrap provider.request() on EIP-1193 wallet providers to track signature and transaction events. This is a read-only observation layer:
- Requests are always forwarded unchanged to the original
provider.request()— parameters and return values are never modified - Tracking is fire-and-forget — event capture runs in an async IIFE and never blocks or delays the wallet call
- Errors are re-thrown unchanged — if the wallet provider throws (e.g. user rejects), the error propagates to the caller exactly as it would without the SDK
- Wrapping is idempotent — the SDK checks for a
WRAPPED_REQUEST_SYMBOLmarker to prevent double-wrapping - Wrapping is graceful — if
provider.requestis immutable (e.g. frozen by the wallet), the SDK silently skips wrapping
Provider wrapping can be fully bypassed in three ways:
-
Disable all autocapture:
FormoAnalytics.init(writeKey, { autocapture: false });
No wallet events are tracked.
provider.request()is never touched. -
Disable only signature and transaction tracking:
FormoAnalytics.init(writeKey, { autocapture: { signature: false, transaction: false } });
Connect, disconnect, and chain-change events still work via standard EIP-1193 event listeners (
accountsChanged,chainChanged,connect,disconnect) — these do not wrapprovider.request(). -
Use Wagmi integration:
FormoAnalytics.init(writeKey, { wagmi: { config } });
In Wagmi mode, the SDK hooks into Wagmi's state management (
config.subscribe()) and mutation cache instead of wrapping any provider.provider.request()is never modified. This is the recommended integration for React/Next.js apps using Wagmi v2.
- Consent cookies use
Secure(HTTPS-only) andSameSite=Strictattributes - Cookie names are derived from a SHA-256 hash of the project write key, providing project isolation
- Session cookies (wallet detection, current URL) are scoped to the current host only
- The
crossSubdomainCookiesoption (defaulttrue) controls whether identity cookies are set on the apex domain; set tofalseto restrict to the current host
The SDK respects user consent preferences:
formo.optOutTracking()— sets a persistent consent flag and stops all event collectionformo.optInTracking()— clears the opt-out flagformo.hasOptedOutTracking()— check current consent status
Consent flags are stored in project-specific cookies and persist across sessions.
| Category | Data | When |
|---|---|---|
| Page | URL, title, path, referrer, UTM parameters | Every page view |
| Browser | User agent, browser name, locale, timezone, screen dimensions | Every event (as context) |
| Wallet | Wallet address, chain ID, provider name | On connect/identify |
| Transaction | Status, chain ID, address, tx hash, calldata, function name/args | On eth_sendTransaction (if autocapture enabled) |
| Signature | Status, chain ID, address, message content, signature hash | On personal_sign / eth_signTypedData_v4 (if autocapture enabled) |
| Identity | Anonymous ID (UUID), optional user ID | Every event |
- Disable EVM tracking entirely:
{ evm: false } - Disable specific event types:
{ autocapture: { transaction: false, signature: false } } - Exclude specific hosts/paths/chains:
{ tracking: { excludeHosts: [...], excludePaths: [...], excludeChains: [...] } } - Use a first-party proxy:
{ apiHost: 'https://your-domain.com/ingest' }— route events through your own infrastructure for inspection and control
- All releases are published with npm provenance via GitHub Actions OIDC — each package can be traced back to the exact source commit and CI workflow
- GitHub Actions are pinned to commit SHAs (not mutable tags)
- Dependencies are pinned and regularly audited
- A lockfile is maintained and enforced in CI (
--frozen-lockfile) - The SDK has minimal dependencies — core cryptographic operations use
ethereum-cryptography