From 0c45ad3f2266a42759d4d2f70d0f7e7e09cd3af3 Mon Sep 17 00:00:00 2001 From: Sanja Malovic <48502315+unjica@users.noreply.github.com> Date: Tue, 20 Oct 2026 10:32:43 +0200 Subject: [PATCH 1/5] docs: add XERJ Corpus Hub contributor badge to READMEs (#840) --- README.de.md | 1 + README.es.md | 1 + README.fr.md | 1 + README.md | 1 + 4 files changed, 4 insertions(+) diff --git a/README.de.md b/README.de.md index 71573e7f..55d418c6 100644 --- a/README.de.md +++ b/README.de.md @@ -10,6 +10,7 @@ ![TypeScript](https://img.shields.io/badge/TypeScript-3178C6?style=flat&logo=typescript&logoColor=white) [![npm version](https://img.shields.io/npm/v/@telemetry-tracker/core)](https://www.npmjs.com/package/@telemetry-tracker/core) [![npm downloads](https://img.shields.io/npm/dm/@telemetry-tracker/core)](https://www.npmjs.com/package/@telemetry-tracker/core) +[![XERJ Corpus Hub contributor](https://hub.xerj.org/contributors/unjica/badge.svg)](https://hub.xerj.org/contributors/unjica.html)

Open-source Fehlerverfolgung, Produktanalyse, und Sitzungstelemetrie. diff --git a/README.es.md b/README.es.md index 164c7b4d..2d586445 100644 --- a/README.es.md +++ b/README.es.md @@ -10,6 +10,7 @@ ![TypeScript](https://img.shields.io/badge/TypeScript-3178C6?style=flat&logo=typescript&logoColor=white) [![npm version](https://img.shields.io/npm/v/@telemetry-tracker/core)](https://www.npmjs.com/package/@telemetry-tracker/core) [![npm downloads](https://img.shields.io/npm/dm/@telemetry-tracker/core)](https://www.npmjs.com/package/@telemetry-tracker/core) +[![XERJ Corpus Hub contributor](https://hub.xerj.org/contributors/unjica/badge.svg)](https://hub.xerj.org/contributors/unjica.html)

Seguimiento de errores, análisis de productos y telemetría de sesiones open-source. diff --git a/README.fr.md b/README.fr.md index a64b041d..a8856111 100644 --- a/README.fr.md +++ b/README.fr.md @@ -10,6 +10,7 @@ ![TypeScript](https://img.shields.io/badge/TypeScript-3178C6?style=flat&logo=typescript&logoColor=white) [![npm version](https://img.shields.io/npm/v/@telemetry-tracker/core)](https://www.npmjs.com/package/@telemetry-tracker/core) [![npm downloads](https://img.shields.io/npm/dm/@telemetry-tracker/core)](https://www.npmjs.com/package/@telemetry-tracker/core) +[![XERJ Corpus Hub contributor](https://hub.xerj.org/contributors/unjica/badge.svg)](https://hub.xerj.org/contributors/unjica.html)

Suivi d'erreurs open-source, analytics produit et télémétrie de session. diff --git a/README.md b/README.md index c098fe3e..ba709422 100644 --- a/README.md +++ b/README.md @@ -10,6 +10,7 @@ ![TypeScript](https://img.shields.io/badge/TypeScript-3178C6?style=flat&logo=typescript&logoColor=white) [![npm version](https://img.shields.io/npm/v/@telemetry-tracker/core)](https://www.npmjs.com/package/@telemetry-tracker/core) [![npm downloads](https://img.shields.io/npm/dm/@telemetry-tracker/core)](https://www.npmjs.com/package/@telemetry-tracker/core) +[![XERJ Corpus Hub contributor](https://hub.xerj.org/contributors/unjica/badge.svg)](https://hub.xerj.org/contributors/unjica.html)

Open-source error tracking, product analytics, and session telemetry. From 64c62a7bec2c53f01c3f7d86c90045af3e54a077 Mon Sep 17 00:00:00 2001 From: Sanja Malovic <48502315+unjica@users.noreply.github.com> Date: Tue, 20 Oct 2026 13:45:14 +0200 Subject: [PATCH 2/5] fix(alerts): explicit all-events vs specific-event scope for heartbeat rules (#844) --- .../app/dashboard/alerts/AlertRuleEditor.tsx | 58 +++++++++- .../lib/alert-rules-heartbeat.test.ts | 100 +++++++++++++++++- apps/dashboard/lib/alert-rules.ts | 25 ++++- 3 files changed, 177 insertions(+), 6 deletions(-) diff --git a/apps/dashboard/app/dashboard/alerts/AlertRuleEditor.tsx b/apps/dashboard/app/dashboard/alerts/AlertRuleEditor.tsx index c978799f..378667c5 100644 --- a/apps/dashboard/app/dashboard/alerts/AlertRuleEditor.tsx +++ b/apps/dashboard/app/dashboard/alerts/AlertRuleEditor.tsx @@ -167,7 +167,41 @@ export function AlertRuleEditor({ {condition.type === "NO_EVENTS" ? ( <> +

+ Watch +
+ + +
+
+ {condition.eventScope === "specific" ? ( + ) : null}

- Exact name as sent with trackEvent(); leave empty - for any event. Pick a window comfortably longer than the usual + {condition.eventScope === "specific" ? ( + <> + Exact name as sent with trackEvent().{" "} + + ) : ( + <>Alerts when no event of any name arrives. + )} + Pick a window comfortably longer than the usual gap between these events (max {MAX_NO_EVENTS_WINDOW_HOURS}h) — quiet apps can go hours without a signup. While the event stays missing the alert repeats every cooldown; there is no recovery @@ -414,6 +462,12 @@ export function AlertRuleEditor({ pending || !draft.name.trim() || draft.conditions.length === 0 || + draft.conditions.some( + (c) => + c.type === "NO_EVENTS" && + c.eventScope === "specific" && + !c.eventName.trim() + ) || draft.destinationIds.length === 0 } onClick={onSubmit} diff --git a/apps/dashboard/lib/alert-rules-heartbeat.test.ts b/apps/dashboard/lib/alert-rules-heartbeat.test.ts index 85ac549f..1ffbe02f 100644 --- a/apps/dashboard/lib/alert-rules-heartbeat.test.ts +++ b/apps/dashboard/lib/alert-rules-heartbeat.test.ts @@ -11,6 +11,7 @@ import { updateDraftCondition, validateAlertRuleDraft, type AlertRuleRow, + type AlertRuleFormDraft, } from "./alert-rules"; const baseRule = { @@ -99,11 +100,11 @@ describe("NO_EVENTS business heartbeats (dashboard)", () => { }); }); - it("omits eventName when left empty (any event)", () => { + it("omits eventName for the explicit all-events scope", () => { const result = validateAlertRuleDraft({ ...createDefaultAlertRuleDraft(), name: "Any", - conditions: [createEmptyNoEventsCondition({ windowHours: 2 })], + conditions: [createEmptyNoEventsCondition({ windowHours: 2, eventScope: "all" })], }); expect(result.ok && result.payload.conditions[0]).toEqual({ type: "NO_EVENTS", @@ -175,3 +176,98 @@ describe("NO_EVENTS business heartbeats (dashboard)", () => { ).toBe('no "signup_verified" event / 6h in production'); }); }); + +describe("NO_EVENTS event scope: all events vs a specific event", () => { + function draftWith( + cond: Parameters[0] + ): AlertRuleFormDraft { + return { + ...createDefaultAlertRuleDraft(), + name: "Heartbeat", + conditions: [createEmptyNoEventsCondition(cond)], + }; + } + + it("new heartbeat conditions default to a specific event", () => { + expect(createEmptyNoEventsCondition().eventScope).toBe("specific"); + }); + + it("'all events' sends no eventName, even if a name was typed earlier", () => { + for (const eventName of ["", "signup_verified"]) { + const result = validateAlertRuleDraft( + draftWith({ eventScope: "all", eventName }) + ); + expect(result.ok).toBe(true); + if (!result.ok) continue; + expect(result.payload.conditions[0]).not.toHaveProperty("eventName"); + } + }); + + it("'specific event' requires a non-empty name", () => { + for (const eventName of ["", " "]) { + const result = validateAlertRuleDraft( + draftWith({ eventScope: "specific", eventName }) + ); + expect(result.ok).toBe(false); + if (result.ok) continue; + expect(result.error).toMatch(/event name/i); + } + }); + + it("'specific event' sends the trimmed name", () => { + const result = validateAlertRuleDraft( + draftWith({ eventScope: "specific", eventName: " signup_verified " }) + ); + expect(result.ok).toBe(true); + if (!result.ok) return; + expect(result.payload.conditions[0]).toMatchObject({ + eventName: "signup_verified", + }); + }); + + it("existing rules without eventName open as 'all events' and round-trip unchanged", () => { + for (const extra of [{}, { eventName: null }]) { + const rule = { + ...baseRule, + conditions: [ + { type: "NO_EVENTS", windowMinutes: 360, environment: null, ...extra }, + ], + } as AlertRuleRow; + const draft = draftFromAlertRule(rule); + const cond = draft.conditions[0]!; + expect(cond.type).toBe("NO_EVENTS"); + if (cond.type !== "NO_EVENTS") continue; + expect(cond.eventScope).toBe("all"); + const result = validateAlertRuleDraft(draft); + expect(result.ok).toBe(true); + if (!result.ok) continue; + expect(result.payload.conditions[0]).not.toHaveProperty("eventName"); + } + }); + + it("rules with an eventName open as 'specific event'", () => { + const rule = { + ...baseRule, + conditions: [ + { + type: "NO_EVENTS", + windowMinutes: 360, + environment: null, + eventName: "signup_verified", + }, + ], + } as AlertRuleRow; + const cond = draftFromAlertRule(rule).conditions[0]!; + expect(cond).toMatchObject({ eventScope: "specific", eventName: "signup_verified" }); + }); + + it("switching scope keeps the typed name in the draft", () => { + let draft = draftWith({ eventScope: "specific", eventName: "signup_verified" }); + draft = updateDraftCondition(draft, 0, { eventScope: "all" }); + draft = updateDraftCondition(draft, 0, { eventScope: "specific" }); + expect(draft.conditions[0]).toMatchObject({ + eventScope: "specific", + eventName: "signup_verified", + }); + }); +}); diff --git a/apps/dashboard/lib/alert-rules.ts b/apps/dashboard/lib/alert-rules.ts index bddef08c..142a0690 100644 --- a/apps/dashboard/lib/alert-rules.ts +++ b/apps/dashboard/lib/alert-rules.ts @@ -118,11 +118,19 @@ export type ErrorCountConditionDraft = { environment: string; }; +/** Which events a NO_EVENTS condition watches. */ +export type NoEventsEventScope = "all" | "specific"; + /** "No `eventName` event for `windowHours` hours" (business heartbeat). */ export type NoEventsConditionDraft = { key: string; type: "NO_EVENTS"; - /** Empty = any event (original NO_EVENTS behaviour). */ + /** + * "all" = any event in the project (original NO_EVENTS behaviour, no + * `eventName` sent). "specific" = only events named `eventName` (required). + */ + eventScope: NoEventsEventScope; + /** Used only when `eventScope` is "specific"; kept while toggling scope. */ eventName: string; windowHours: number; environment: string; @@ -186,6 +194,11 @@ export function createEmptyNoEventsCondition( return { key: nextDraftKey(), type: "NO_EVENTS", + eventScope: + overrides?.eventScope ?? + (overrides?.eventName !== undefined && overrides.eventName.trim() === "" + ? "all" + : "specific"), eventName: overrides?.eventName ?? "", windowHours: overrides?.windowHours ?? DEFAULT_NO_EVENTS_WINDOW_HOURS, environment: overrides?.environment ?? "", @@ -274,6 +287,7 @@ export function updateDraftCondition( } return { ...c, + ...(patch.eventScope !== undefined ? { eventScope: patch.eventScope } : {}), ...(patch.eventName !== undefined ? { eventName: patch.eventName } : {}), ...(patch.windowHours !== undefined ? { windowHours: patch.windowHours } @@ -330,6 +344,7 @@ export function draftFromAlertRule(rule: AlertRuleRow): AlertRuleFormDraft { } else if (c.type === "NO_EVENTS" && noEventsConditionIsEditable(c)) { editableConditions.push( createEmptyNoEventsCondition({ + eventScope: c.eventName ? "specific" : "all", eventName: c.eventName ?? "", windowHours: c.windowMinutes / 60, environment: c.environment ?? "", @@ -510,7 +525,13 @@ export function validateAlertRuleDraft( error: `Condition #${i + 1}: window must be ${MIN_NO_EVENTS_WINDOW_HOURS}–${MAX_NO_EVENTS_WINDOW_HOURS} hours`, }; } - const eventName = c.eventName.trim(); + const eventName = c.eventScope === "specific" ? c.eventName.trim() : ""; + if (c.eventScope === "specific" && eventName.length === 0) { + return { + ok: false, + error: `Condition #${i + 1}: enter the event name, or choose "All events"`, + }; + } if (eventName.length > MAX_ALERT_RULE_EVENT_NAME_LENGTH) { return { ok: false, From 9c3b87096822d44e11503eff71e1b3d52345d9e8 Mon Sep 17 00:00:00 2001 From: Sanja Malovic <48502315+unjica@users.noreply.github.com> Date: Tue, 20 Oct 2026 15:43:21 +0200 Subject: [PATCH 3/5] feat(marketing): missing-event alerts landing page (/features/missing-event-alerts) (#842) * feat(marketing): add missing-event alerts (business heartbeats) landing page * feat(marketing): list verified heartbeat alert channels, trim setup duplication --------- Co-authored-by: Telemetry Tracker Bot --- .../components/marketing/guides/GuideCta.tsx | 4 +- apps/dashboard/app/docs/alerts/page.tsx | 5 +- .../features/missing-event-alerts/page.tsx | 291 ++++++++++++++++++ apps/dashboard/app/llms.txt/route.ts | 1 + apps/dashboard/lib/marketing-guides.test.ts | 1 + apps/dashboard/lib/public-seo-paths.test.ts | 1 + apps/dashboard/lib/public-seo-paths.ts | 4 +- 7 files changed, 303 insertions(+), 4 deletions(-) create mode 100644 apps/dashboard/app/features/missing-event-alerts/page.tsx diff --git a/apps/dashboard/app/components/marketing/guides/GuideCta.tsx b/apps/dashboard/app/components/marketing/guides/GuideCta.tsx index 9a8fb828..74f1c805 100644 --- a/apps/dashboard/app/components/marketing/guides/GuideCta.tsx +++ b/apps/dashboard/app/components/marketing/guides/GuideCta.tsx @@ -6,11 +6,13 @@ export function GuideCta({ body = "No credit card. Create a project, install the SDK, and the first error shows up in Issues.", secondaryHref = "/docs/hosted-cloud", secondaryLabel = "Hosted cloud guide", + primaryLabel = "Start free", }: { heading?: string; body?: string; secondaryHref?: string; secondaryLabel?: string; + primaryLabel?: string; }) { return (

@@ -22,7 +24,7 @@ export function GuideCta({ pendingLabel="Loading…" className="inline-flex items-center gap-1.5 rounded-full bg-primary px-5 py-2.5 text-sm font-medium text-primary-foreground transition-transform hover:scale-[1.02]" > - Start free + {primaryLabel} ERROR_RATE between error ingests -

Business heartbeats

+

Business heartbeats

Some failures throw nothing: a verification email is never sent, a webhook returns 200 but skips the work, a nightly job never runs. Track an event when the flow succeeds, then alert - when that event has not arrived for a number of hours. + when that event has not arrived for a number of hours. Examples and limits are on{" "} + missing-event alerts.

{`trackEvent("signup_verified");          // user confirmed their email
 trackEvent("stripe_webhook_processed"); // webhook handler finished the work
diff --git a/apps/dashboard/app/features/missing-event-alerts/page.tsx b/apps/dashboard/app/features/missing-event-alerts/page.tsx
new file mode 100644
index 00000000..2f4a3bad
--- /dev/null
+++ b/apps/dashboard/app/features/missing-event-alerts/page.tsx
@@ -0,0 +1,291 @@
+import Link from "next/link";
+import { CodeBlock } from "@/app/components/docs/CodeBlock";
+import { GuideCta } from "@/app/components/marketing/guides/GuideCta";
+import { GuideJsonLd } from "@/app/components/marketing/guides/GuideJsonLd";
+import { GuideRelatedLinks } from "@/app/components/marketing/guides/GuideRelatedLinks";
+import { MarketingGuideLayout } from "@/app/components/marketing/guides/MarketingGuideLayout";
+import {
+  breadcrumbJsonLd,
+  faqPageJsonLd,
+  marketingGuideMetadata,
+  webPageJsonLd,
+} from "@/lib/marketing-guide-metadata";
+import { marketingSiteOrigin } from "@/lib/marketing-json-ld";
+
+const PATH = "/features/missing-event-alerts";
+const TITLE = "Alert When Events Stop Arriving: Missing-Event Alerts";
+const H1 = "Get alerted when an event you expect stops arriving";
+const DESCRIPTION =
+  "Get an email when an expected event, like signup_verified or a processed webhook, hasn't arrived for 1–24 hours. Built on the trackEvent calls you already send.";
+
+const FAQ = [
+  {
+    question: "What is a business heartbeat?",
+    answer:
+      "A custom event your app sends when a key flow succeeds. We alert you when none arrive in the window you choose.",
+  },
+  {
+    question: "Does this monitor my payments or signups automatically?",
+    answer: "No. You send the event from your code. We only see the events you track.",
+  },
+  {
+    question: "How quickly will I hear about it?",
+    answer:
+      "After the window passes, on the next scheduled evaluation (by default every 5 minutes). It is not real-time.",
+  },
+  {
+    question: "Can I limit it to production?",
+    answer: "Yes. Each rule has an optional environment filter.",
+  },
+  {
+    question: "Do I get a recovery alert?",
+    answer: "No. The alerts stop when the events come back.",
+  },
+  {
+    question: "How is this different from cron check-ins?",
+    answer:
+      "Check-ins are for scheduled jobs, with ping URLs and deadlines. Heartbeats are for events your app flows send.",
+  },
+] as const;
+
+const TRACK = `import { trackEvent } from "@telemetry-tracker/node"; // or @telemetry-tracker/next, @telemetry-tracker/react-native
+
+// Call it after the flow actually succeeded
+trackEvent("signup_verified");`;
+
+const linkClass = "text-brand hover:underline";
+
+export function generateMetadata() {
+  return marketingGuideMetadata({
+    title: TITLE,
+    description: DESCRIPTION,
+    path: PATH,
+    omitBrandSuffix: true,
+  });
+}
+
+export default function MissingEventAlertsPage() {
+  const origin = marketingSiteOrigin();
+
+  return (
+    <>
+      
+      
+            Error tracking only sees code that throws. Some failures throw nothing: the verification
+            email never goes out, a webhook returns 200 but skips the work, a nightly job never
+            runs. We call the fix business heartbeats: send an event when the flow succeeds, and
+            we'll email you when it goes missing.
+          
+        }
+      >
+        

How it works

+
    +
  1. + Call trackEvent("signup_verified") after the flow succeeds, with + the{" "} + + Node + + ,{" "} + + Next.js + {" "} + or{" "} + + React Native + {" "} + SDK. +
  2. +
  3. + Add a No events (heartbeat) alert rule for that exact event name, with + a window of 1–24 hours and an optional environment. +
  4. +
  5. + When the event goes missing, the alert names it (for example{" "} + no "signup_verified" events in the last 6 hours in production) and + links to the Events page filtered to that event. +
  6. +
+ +

+ We email you by default. The same alert also shows up in your in-app notifications, and + you can send it to HTTPS webhooks, Slack, Discord, Microsoft Teams or Telegram channels + you have set up for the project. +

+

+ Step-by-step setup, including the API version of the rule, is in the{" "} + + business heartbeats setup + {" "} + docs. Every event goes through the{" "} + + SDK + {" "} + you already use for errors, so the alert sits next to your issues. +

+ +

What to monitor

+

Signup verification

+

+ Send signup_verified when a user confirms their email. A heartbeat can tell + you when those stop arriving, for example because verification emails stopped going out. + A small app can go hours without a signup, so start with a 6–12 hour window. +

+

Payment webhook processed

+

+ Send stripe_webhook_processed after your handler did the work, not when the + request arrives. A handler that returns 200 and skips the event then shows up as missing. + Pick a window a few times your normal gap between webhooks. +

+

Nightly job finished

+

+ Send daily_report_finished as the last line of the job. The window can be at + most 24 hours, which leaves little slack for a daily job. For job deadlines, consider{" "} + + check-ins + {" "} + instead. +

+ +

Choosing a window

+
    +
  • Make it comfortably longer than the normal gap between those events.
  • +
  • Check the Events page first to see how often the event really arrives.
  • +
  • + At low traffic, quiet nights and weekends cause false alerts if the window is too short. +
  • +
  • The dashboard defaults to 6 hours, with a cooldown equal to the window.
  • +
+ +

Limits

+

We'd rather you know these up front:

+
    +
  • Each condition watches one exact event name.
  • +
  • The window is at most 24 hours.
  • +
  • + It is not real-time. Rules are checked about every 5 minutes, so + you hear about it after your window plus up to one check. +
  • +
  • + Recovery notifications aren't supported yet. While the event stays missing, the rule fires again + every cooldown, and it stops when events come back. +
  • +
  • There is no learned baseline or anomaly detection. You choose the window.
  • +
  • + You instrument and send the event yourself. Nothing is monitored that you don't + track, so we can't guarantee that your signups or payments are covered. +
  • +
  • + If you{" "} + + self-host + + , you need to run the alert-rules evaluator cron. +
  • +
+ +

Heartbeats vs check-ins

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ Business heartbeatCheck-ins
SignalCustom event via the SDKRequest to a secret ping URL
Best forIrregular, user-driven flowsCron and time-zone schedules, runtime deadlines
Configured asAn alert ruleA check-in monitor
RecoveryNo recovery notificationDown and recovery states
+
+

+ See{" "} + + cron and background-job check-ins + {" "} + for scheduled jobs. +

+ +

FAQ

+ {FAQ.map((item) => ( +
+

{item.question}

+

{item.answer}

+
+ ))} + + +

+ Plans are on{" "} + + pricing + + . Questions? Write to{" "} + + info@telemetry-tracker.com + + . +

+ +
+ + ); +} diff --git a/apps/dashboard/app/llms.txt/route.ts b/apps/dashboard/app/llms.txt/route.ts index e6f9945d..0394894f 100644 --- a/apps/dashboard/app/llms.txt/route.ts +++ b/apps/dashboard/app/llms.txt/route.ts @@ -43,6 +43,7 @@ export function GET() { - ${origin}/docs/hosted-cloud - ${origin}/error-tracking/react-native/expo-source-maps - ${origin}/sentry-alternative +- ${origin}/features/missing-event-alerts ## Not for model training diff --git a/apps/dashboard/lib/marketing-guides.test.ts b/apps/dashboard/lib/marketing-guides.test.ts index 15773854..044f68bf 100644 --- a/apps/dashboard/lib/marketing-guides.test.ts +++ b/apps/dashboard/lib/marketing-guides.test.ts @@ -17,6 +17,7 @@ const pageFiles: Record<(typeof MARKETING_GUIDE_PATHS)[number], string> = { "/error-tracking/react-native": "error-tracking/react-native/page.tsx", "/error-tracking/react-native/expo-source-maps": "error-tracking/react-native/expo-source-maps/page.tsx", + "/features/missing-event-alerts": "features/missing-event-alerts/page.tsx", }; describe("marketing guide pages", () => { diff --git a/apps/dashboard/lib/public-seo-paths.test.ts b/apps/dashboard/lib/public-seo-paths.test.ts index 76ab3ace..eb89a655 100644 --- a/apps/dashboard/lib/public-seo-paths.test.ts +++ b/apps/dashboard/lib/public-seo-paths.test.ts @@ -24,6 +24,7 @@ describe("public SEO paths", () => { expect(PUBLIC_SEO_PATHS).toContain( "/error-tracking/nextjs/server-components-render-error" ); + expect(PUBLIC_SEO_PATHS).toContain("/features/missing-event-alerts"); expect(PUBLIC_SEO_PATHS).not.toContain("/error-tracking/nextjs/source-maps"); }); diff --git a/apps/dashboard/lib/public-seo-paths.ts b/apps/dashboard/lib/public-seo-paths.ts index 159eb06e..3a061c1a 100644 --- a/apps/dashboard/lib/public-seo-paths.ts +++ b/apps/dashboard/lib/public-seo-paths.ts @@ -36,6 +36,7 @@ export const PUBLIC_SEO_PATHS = [ "/error-tracking/nodejs", "/error-tracking/react-native", "/error-tracking/react-native/expo-source-maps", + "/features/missing-event-alerts", ] as const; /** Affiliate program pages: indexable, but only listed when NEXT_PUBLIC_AFFILIATES_ENABLED=true. */ @@ -59,10 +60,11 @@ export const MARKETING_GUIDE_PATHS = [ "/error-tracking/nodejs", "/error-tracking/react-native", "/error-tracking/react-native/expo-source-maps", + "/features/missing-event-alerts", ] as const; /** Content date for sitemap lastmod. Bump when public pages change. */ -export const SITEMAP_LAST_MODIFIED = new Date("2026-10-19T00:00:00.000Z"); +export const SITEMAP_LAST_MODIFIED = new Date("2026-10-20T00:00:00.000Z"); export function sitemapPriority(path: PublicSeoPath): number { if (path === "") return 1; From d002a914b5f1f60a2588a7c273435a1de69a1287 Mon Sep 17 00:00:00 2001 From: Sanja Malovic <48502315+unjica@users.noreply.github.com> Date: Tue, 20 Oct 2026 15:51:12 +0200 Subject: [PATCH 4/5] docs: use info@telemetry-tracker.com as example sender instead of noreply@tacko.io (#845) --- apps/api/scripts/send-release-email.ts | 2 +- apps/api/src/routes/contact.ts | 2 +- docs/BILLING.md | 6 +++--- docs/MARKETING-EMAIL.md | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/apps/api/scripts/send-release-email.ts b/apps/api/scripts/send-release-email.ts index bd2bd329..8b93144b 100644 --- a/apps/api/scripts/send-release-email.ts +++ b/apps/api/scripts/send-release-email.ts @@ -288,7 +288,7 @@ async function main() { if (isResendDomainVerificationError(result.error)) { console.error( "Resend rejected the sender domain. Set TELEMETRY_EMAIL_FROM to an address on a verified domain " + - "(hosted cloud: Telemetry Tracker ). See docs/BILLING.md#production-setup-hosted-cloud." + "(hosted cloud: Telemetry Tracker ). See docs/BILLING.md#production-setup-hosted-cloud." ); process.exit(1); } diff --git a/apps/api/src/routes/contact.ts b/apps/api/src/routes/contact.ts index 03d10d1b..4d7fe505 100644 --- a/apps/api/src/routes/contact.ts +++ b/apps/api/src/routes/contact.ts @@ -81,7 +81,7 @@ function contactDeliveryError(result: { status?: number; error?: string }): stri ) { return ( `Email delivery is blocked by Resend: verify tacko.io (or your sending domain) in the Resend dashboard ` + - `and set TELEMETRY_EMAIL_FROM to an address on that domain (e.g. Telemetry ). ` + + `and set TELEMETRY_EMAIL_FROM to an address on that domain (e.g. Telemetry ). ` + `Until then, email ${CONTACT_INBOX} directly.` ); } diff --git a/docs/BILLING.md b/docs/BILLING.md index eefaa17e..7ca875bb 100644 --- a/docs/BILLING.md +++ b/docs/BILLING.md @@ -11,7 +11,7 @@ Without email configured, admins must share invite or reset links manually. In p | Variable | Description | |----------|-------------| | `RESEND_API_KEY` | API key from [Resend](https://resend.com) | -| `TELEMETRY_EMAIL_FROM` | Sender address, e.g. `Telemetry Tracker ` | +| `TELEMETRY_EMAIL_FROM` | Sender address, e.g. `Telemetry Tracker ` | Both are required for outbound email. Verify your sending domain in Resend for production. @@ -60,7 +60,7 @@ The hosted cloud sends from **tacko.io** (operator domain). `telemetry-tracker.c Recommended sender: ```env -TELEMETRY_EMAIL_FROM=Telemetry Tracker +TELEMETRY_EMAIL_FROM=Telemetry Tracker CONTACT_INBOX_EMAIL=info@telemetry-tracker.com ``` @@ -73,7 +73,7 @@ On the **API** Railway service (not dashboard), set: | Variable | Example | |----------|---------| | `RESEND_API_KEY` | `re_…` | -| `TELEMETRY_EMAIL_FROM` | `Telemetry Tracker ` | +| `TELEMETRY_EMAIL_FROM` | `Telemetry Tracker ` | | `CONTACT_INBOX_EMAIL` | `info@telemetry-tracker.com` | | `TELEMETRY_DASHBOARD_ORIGIN` | `https://telemetry-tracker.com` | diff --git a/docs/MARKETING-EMAIL.md b/docs/MARKETING-EMAIL.md index 8945400c..c5339a0c 100644 --- a/docs/MARKETING-EMAIL.md +++ b/docs/MARKETING-EMAIL.md @@ -100,7 +100,7 @@ Required GitHub secrets in the **`production`** environment: |--------|---------| | `DATABASE_URL` | Production Postgres **public** URL (Railway TCP proxy / public host — not `postgres.railway.internal`) | | `RESEND_API_KEY` | Resend API | -| `TELEMETRY_EMAIL_FROM` | From address on a **verified** Resend domain — use `Telemetry Tracker `, not `@telemetry-tracker.com` (see [BILLING.md → Production setup](./BILLING.md#production-setup-hosted-cloud)) | +| `TELEMETRY_EMAIL_FROM` | From address on a **verified** Resend domain — e.g. `Telemetry Tracker ` (telemetry-tracker.com is verified in Resend) (see [BILLING.md → Production setup](./BILLING.md#production-setup-hosted-cloud)) | | `TELEMETRY_DASHBOARD_ORIGIN` | Unsubscribe / docs links (e.g. `https://telemetry-tracker.com`) — app URL only; does not set the mail-from domain | Store these in the GitHub **`production`** environment. Finalize `CHANGELOG.md` on `main` **before** the closing tag so the composed line body matches what shipped. From 60a9abfb0b35f213bb07de3a334c38cdc6ebe825 Mon Sep 17 00:00:00 2001 From: unjica <48502315+unjica@users.noreply.github.com> Date: Tue, 20 Oct 2026 15:51:32 +0200 Subject: [PATCH 5/5] release: v1.22.11 --- CHANGELOG.md | 14 ++++++++++++++ apps/api/src/generated/api-version.ts | 2 +- 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d9d577e9..9948e79e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,20 @@ Contributors: add user-facing changes under **[Unreleased]** in your PR to `deve --- +## [1.22.11] - 2026-10-20 + +### Added + +- **Missing-event alerts page** — new marketing page at `/features/missing-event-alerts` describing business heartbeat rules (one named event per condition, 1–24 hour window, checks about every 5 minutes, no recovery notification yet), linking to `/docs/alerts` and `/docs/checkins`; added to the sitemap and `llms.txt` ([#842](https://github.com/Telemetry-Tracker/telemetry-tracker/pull/842)) +- **README** — XERJ Corpus Hub contributor badge in the README and its German, Spanish and French versions ([#840](https://github.com/Telemetry-Tracker/telemetry-tracker/pull/840)) + +### Changed + +- **Heartbeat rule editor** — a "No events" condition now asks explicitly whether to watch all events in the project or a specific event; "A specific event" requires a name. Existing rules without an event name open as "All events" and behave as before. No API change ([#844](https://github.com/Telemetry-Tracker/telemetry-tracker/pull/844)) +- **Docs** — example sender in the email setup docs is now `Telemetry Tracker ` instead of `noreply@tacko.io` ([#845](https://github.com/Telemetry-Tracker/telemetry-tracker/pull/845)) + +--- + ## [1.22.10] - 2026-10-20 ### Added diff --git a/apps/api/src/generated/api-version.ts b/apps/api/src/generated/api-version.ts index 1aa0af41..669d970f 100644 --- a/apps/api/src/generated/api-version.ts +++ b/apps/api/src/generated/api-version.ts @@ -1,2 +1,2 @@ /** Generated at build time from CHANGELOG (CHANGELOG.md). Do not edit manually. */ -export const API_VERSION = "1.22.10"; +export const API_VERSION = "1.22.11";