Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/telemetry-error-tracking.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@opencoredev/email-sdk": minor
---

Add anonymous usage telemetry and redacted error reporting to the SDK and CLI via PostHog. The client reports `client created` (configured adapter names), `email sent` (adapter, success/failure, error code, duration, recipient count, whether recipient variables or `sendAt` were used, and the delivery path: single, native bulk, or per-recipient expansion), an `email batch sent` summary for `sendBatch`, and the CLI reports `cli command run` (command, adapter, success). A `source` property distinguishes CLI runs from library usage, and CI providers are detected. Error reports carry only the error type, Email SDK error code, and stack frames with package-relative file names; messages are scrubbed of email addresses, URLs, quoted text, tokens, and home directories before upload. No email content, addresses, headers, or credentials are ever collected, and custom adapter names are masked as `custom`. A one-time notice with opt-out instructions is printed on first use. Opt out with `EMAIL_SDK_TELEMETRY=0`, `DO_NOT_TRACK=1`, or `createEmailClient({ telemetry: false })`; telemetry is disabled automatically when `NODE_ENV=test`.
20 changes: 20 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,26 @@ jobs:
env:
GITHUB_TOKEN: ${{ secrets.CHANGESETS_TOKEN }}

- name: Annotate release in PostHog
if: steps.changesets.outputs.published == 'true'
env:
POSTHOG_PERSONAL_API_KEY: ${{ secrets.POSTHOG_PERSONAL_API_KEY }}
run: |
if [ -z "$POSTHOG_PERSONAL_API_KEY" ]; then
echo "::notice::POSTHOG_PERSONAL_API_KEY not set; skipping release annotation."
exit 0
fi
VERSION="$(bun -e 'const pkg = await Bun.file("packages/email-sdk/package.json").json(); console.log(pkg.version)')"
PAYLOAD="$(jq -n \
--arg content "@opencoredev/email-sdk v${VERSION} released" \
--arg date "$(date -u +%Y-%m-%dT%H:%M:%SZ)" \
'{content: $content, date_marker: $date, scope: "project"}')"
curl --fail-with-body -sS -X POST "https://us.posthog.com/api/projects/468042/annotations/" \
-H "Authorization: Bearer $POSTHOG_PERSONAL_API_KEY" \
-H "Content-Type: application/json" \
-d "$PAYLOAD" \
|| echo "::warning::PostHog release annotation failed (non-blocking)."

- name: Update Homebrew formula checksum
if: steps.changesets.outputs.published == 'true'
env:
Expand Down
20 changes: 20 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,26 @@ Full docs live at **[email-sdk.dev/docs](https://email-sdk.dev/docs)**. Good pla
- [Fallbacks and retries](https://email-sdk.dev/docs/concepts/fallbacks-and-retries)
- [Field support](https://email-sdk.dev/docs/adapters/field-support)

## Telemetry

Email SDK collects anonymous usage analytics so we can see which adapters and CLI commands get used and how often sends succeed. The first run prints a notice with opt-out instructions.

What is collected: built-in adapter names (custom adapters are reported as `custom`), CLI command names, success/failure and error codes, send duration, total recipient counts (`to` + `cc` + `bcc`), whether a message includes attachments (a boolean only, never the files themselves), whether a send used recipient variables or scheduling and which delivery path ran, SDK version, OS, Node.js version, whether the run happens in CI (and which CI provider), whether usage comes from the library or the bundled CLI, and redacted error reports — the error type, Email SDK error code, and stack traces with file paths reduced to package-relative names, with error messages scrubbed of email addresses, URLs, quoted text, long tokens, and home directories before upload — tied to a random anonymous ID stored in `~/.config/email-sdk/telemetry.json`. What is never collected: email content, subjects, addresses, headers, attachments, API keys, or any other message data.

Opt out at any time with an environment variable:

```bash
export EMAIL_SDK_TELEMETRY=0 # or DO_NOT_TRACK=1
```

or per client in code:

```ts
const client = createEmailClient({ adapters: [resend({ apiKey })], telemetry: false });
```

Telemetry is also disabled automatically when `NODE_ENV=test`.

## Sponsors

Email SDK is supported by companies that help keep provider integrations practical and maintained. Want your logo here? **[Become a sponsor →](https://github.com/sponsors/opencoredev)**
Expand Down
1 change: 1 addition & 0 deletions apps/fumadocs/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
"fumadocs-ui": "16.9.1",
"lucide-react": "^1.16.0",
"marked": "^18.0.5",
"posthog-js": "^1.386.6",
"react": "^19.2.6",
"react-dom": "^19.2.6",
"sanitize-html": "^2.17.5",
Expand Down
33 changes: 33 additions & 0 deletions apps/fumadocs/src/lib/posthog.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import posthog from "posthog-js";

// Same PostHog project as the SDK/CLI telemetry (write-only public key).
const POSTHOG_PROJECT_KEY = "phc_D62r4m5ivBr6LPCBqjKHg8GL6QTxT57LTzKrmkg5hNZS";

let initialized = false;

export function initPostHog() {
if (typeof window === "undefined" || initialized) {
return;
}

initialized = true;

posthog.init(POSTHOG_PROJECT_KEY, {
api_host: "https://us.i.posthog.com",
// 2026-01-30 defaults capture pageviews on history changes, covering
// TanStack Router client-side navigations without a router subscription.
defaults: "2026-01-30",
capture_exceptions: {
capture_unhandled_errors: true,
capture_unhandled_rejections: true,
// Explicitly off: console.error noise would drown real exceptions.
capture_console_errors: false,
},
capture_performance: { web_vitals: true },
// Docs traffic is anonymous (we never identify), so no person profiles are
// created — mirroring the SDK's server-side $process_person_profile: false.
person_profiles: "identified_only",
// Flip to false (plus a sampling rate in project settings) to enable replay.
disable_session_recording: true,
});
}
5 changes: 5 additions & 0 deletions apps/fumadocs/src/routes/__root.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { StaleBuildNotice } from "@/components/stale-build-notice";
import { chunkLoadGuardScript } from "@/lib/chunk-load-guard";
import { domMutationGuardScript } from "@/lib/dom-mutation-guard";
import { siteMeta } from "@/lib/metadata";
import { initPostHog } from "@/lib/posthog";

import appCss from "@/styles/app.css?url";

Expand Down Expand Up @@ -38,6 +39,10 @@ export const Route = createRootRoute({
});

function RootComponent() {
React.useEffect(() => {
initPostHog();
}, []);

return (
<html lang="en" suppressHydrationWarning>
<head>
Expand Down
21 changes: 15 additions & 6 deletions apps/fumadocs/src/routes/privacy.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -42,9 +42,18 @@ function Privacy() {
<div className="mt-10 space-y-9 text-sm leading-7 text-fd-muted-foreground md:text-base">
<PolicySection title="What We Collect">
We may receive basic website analytics, such as page views, referrers, browser
information, and coarse region data. If you contact the project, open an issue, or
contribute to the repository, we receive the information you choose to provide in that
message or contribution.
information, and coarse region data, along with client-side error reports that help us
fix broken docs pages. The npm package collects its own anonymous, opt-out usage
telemetry described in the{" "}
<a
className="underline"
href="https://github.com/opencoredev/email-sdk#telemetry"
rel="noreferrer"
>
project README
</a>
. If you contact the project, open an issue, or contribute to the repository, we
receive the information you choose to provide in that message or contribution.
</PolicySection>

<PolicySection title="What We Do Not Collect">
Expand All @@ -60,9 +69,9 @@ function Privacy() {
</PolicySection>

<PolicySection title="Third-Party Services">
The site is hosted on Vercel and may use Vercel Web Analytics. Package downloads,
issues, pull requests, and repository activity are handled by npm and GitHub under
their own policies.
The site is hosted on Vercel and may use Vercel Web Analytics and PostHog (US cloud)
for analytics and error monitoring. Package downloads, issues, pull requests, and
repository activity are handled by npm and GitHub under their own policies.
</PolicySection>

<PolicySection title="Contact">
Expand Down
21 changes: 21 additions & 0 deletions bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions bunfig.toml
Original file line number Diff line number Diff line change
@@ -1,2 +1,5 @@
[install]
linker = "isolated"

[test]
preload = ["./packages/email-sdk/test-preload.ts"]
20 changes: 20 additions & 0 deletions packages/email-sdk/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -333,6 +333,26 @@ npx email-sdk send --dry-run --adapter resend --from hello@example.com --to user

The CLI can read provider credentials from environment variables or matching credential flags. Run `bunx --bun --package @opencoredev/email-sdk email-sdk adapters` for a one-off adapter list, or `npx email-sdk adapters` after installing the scoped package in a project. `--dry-run` validates the message and selected adapter field support without sending email.

## Telemetry

Email SDK collects anonymous usage analytics so we can see which adapters and CLI commands get used and how often sends succeed. The first run prints a notice with opt-out instructions.

What is collected: built-in adapter names (custom adapters are reported as `custom`), CLI command names, success/failure and error codes, send duration, total recipient counts (`to` + `cc` + `bcc`), whether a message includes attachments (a boolean only, never the files themselves), whether a send used recipient variables or scheduling and which delivery path ran, SDK version, OS, Node.js version, whether the run happens in CI (and which CI provider), whether usage comes from the library or the bundled CLI, and redacted error reports — the error type, Email SDK error code, and stack traces with file paths reduced to package-relative names, with error messages scrubbed of email addresses, URLs, quoted text, long tokens, and home directories before upload — tied to a random anonymous ID stored in `~/.config/email-sdk/telemetry.json`. What is never collected: email content, subjects, addresses, headers, attachments, API keys, or any other message data.

Opt out at any time with an environment variable:

```bash
export EMAIL_SDK_TELEMETRY=0 # or DO_NOT_TRACK=1
```

or per client in code:

```ts
const client = createEmailClient({ adapters: [resend({ apiKey })], telemetry: false });
```

Telemetry is also disabled automatically when `NODE_ENV=test`.

## Provider Reality

Email providers differ in domain verification, sandbox modes, rate limits, region settings, API scopes, and field support. Email SDK tests the normalized payloads and fail-fast validation locally, but the final live send still depends on provider account configuration.
Expand Down
5 changes: 5 additions & 0 deletions packages/email-sdk/bunfig.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# Bun reads bunfig.toml from the cwd only, so this mirrors the root [test]
# preload for `bun test` runs started inside this package. Install settings
# live in the repo-root bunfig.toml.
[test]
preload = ["./test-preload.ts"]
3 changes: 3 additions & 0 deletions packages/email-sdk/src/cli.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,9 @@ async function runCli(args: string[]) {
const proc = Bun.spawn({
cmd: ["bun", "src/cli.ts", ...args],
cwd: packageRoot,
// NODE_ENV=test already disables telemetry; the explicit opt-out keeps these
// tests network-free even if env propagation changes.
env: { ...process.env, EMAIL_SDK_TELEMETRY: "0" },
stderr: "pipe",
stdout: "pipe",
});
Expand Down
Loading