Skip to content
Open
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 redacted anonymous error reporting (PostHog error tracking) plus richer usage analytics: a `source` property distinguishing CLI runs from library usage, an `email batch sent` summary event, and CI provider detection. 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. All existing opt-outs (`EMAIL_SDK_TELEMETRY=0`, `DO_NOT_TRACK=1`, `telemetry: false`, `NODE_ENV=test`) apply unchanged.
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
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,7 @@ The skill is stored in `skills/email-sdk/SKILL.md`. It tells agents to refresh t

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), SDK version, OS, and Node.js version — 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.
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), 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:

Expand Down
1 change: 1 addition & 0 deletions apps/fumadocs/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
"fumadocs-mdx": "15.0.9",
"fumadocs-ui": "16.9.1",
"lucide-react": "^1.16.0",
"posthog-js": "^1.386.6",
"react": "^19.2.6",
"react-dom": "^19.2.6",
"shiki": "4.1.0",
Expand Down
28 changes: 28 additions & 0 deletions apps/fumadocs/src/lib/posthog.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
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: true,
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 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.

2 changes: 1 addition & 1 deletion packages/email-sdk/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -335,7 +335,7 @@ The CLI can read provider credentials from environment variables or matching cre

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), SDK version, OS, and Node.js version — 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.
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), 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:

Expand Down
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
14 changes: 13 additions & 1 deletion packages/email-sdk/src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -259,7 +259,7 @@ async function main(command: string | undefined, flags: CliFlags) {
}

const provider = createProvider(providerName, flags);
const client = createEmailClient({ adapters: [provider] });
const client = createEmailClient({ adapters: [provider], telemetrySource: "cli" });
const response = await client.send(message);

console.log(JSON.stringify(response, null, 2));
Expand Down Expand Up @@ -707,6 +707,7 @@ async function captureCliRun(input: {
command: normalizeCliCommand(input.command),
adapter: adapter ? normalizeAdapterName(adapter) : undefined,
dry_run: truthyFlag(input.flags, "dry-run"),
source: "cli",
success: input.success,
duration_ms: Date.now() - input.startedAt,
error_code:
Expand All @@ -730,6 +731,17 @@ try {
await main(cliCommand, cliFlags);
await captureCliRun({ command: cliCommand, flags: cliFlags, success: true, startedAt });
} catch (error) {
if (!(error instanceof CliFailure) && !(error instanceof EmailSdkError)) {
// Unexpected crash, not a usage or provider failure. Reported before the run
// summary so captureCliRun's flush() settles it too. Errors rethrown out of
// client.send were already reported there; the per-object dedupe drops this one.
void getTelemetry().captureException(error, {
source: "cli",
handled: false,
command: normalizeCliCommand(cliCommand),
});
}

await captureCliRun({ command: cliCommand, flags: cliFlags, success: false, startedAt, error });

if (error instanceof CliFailure || error instanceof EmailSdkError) {
Expand Down
Loading