Skip to content

feat: Betterstack Importer #2016

Open
aggmoulik wants to merge 5 commits intoopenstatusHQ:mainfrom
aggmoulik:feature/betterstack-importer
Open

feat: Betterstack Importer #2016
aggmoulik wants to merge 5 commits intoopenstatusHQ:mainfrom
aggmoulik:feature/betterstack-importer

Conversation

@aggmoulik
Copy link
Contributor

@aggmoulik aggmoulik commented Mar 26, 2026

Summary

Adds a Better Stack Uptime import provider to the @openstatus/importers package, enabling users to migrate their monitoring setup from Better Stack into OpenStatus.

This is the first importer that can import actual monitors (HTTP checks with URLs, frequency, regions) — the existing Statuspage importer only imports status page data.

What gets imported

Phase Better Stack Source OpenStatus Target
Monitors Uptime monitors (URL, frequency, method, headers, regions) monitor table
Status Page Status page (company name, subdomain, custom domain) page table
Monitor Groups Monitor groups pageComponentGroup table
Sections Status page sections pageComponentGroup table
Incidents Incidents (with started/acknowledged/resolved timestamps) statusReport + statusReportUpdate tables

Field mapping highlights

  • Frequency: Better Stack seconds (30, 60, 300...) mapped to OpenStatus periodicity enum (30s, 1m, 5m...)
  • Regions: Better Stack broad regions (us, eu, as, au) mapped to specific Fly regions (iad, fra, sin, syd)
  • Incidents: Timestamps (started_at, acknowledged_at, resolved_at) converted to synthetic status report updates (investigatingidentifiedresolved)
  • Auth: Bearer token (vs Statuspage's OAuth)
  • Pagination: JSON:API style — follows pagination.next links

Changes

New files (10)

  • packages/importers/src/providers/betterstack/ — Full provider implementation:
    • api-types.ts — Zod schemas for Better Stack API responses
    • client.ts — HTTP client with Bearer auth and JSON:API pagination
    • mapper.ts — 9 transformation functions (monitor, frequency, regions, status page, incidents, etc.)
    • provider.ts — 5-phase import orchestration
    • index.ts — Barrel exports
    • fixtures.ts — Mock data (3 monitors, 1 group, 1 status page, 2 sections, 3 incidents)
    • client.test.ts — 8 tests (endpoints, auth, pagination, errors)
    • mapper.test.ts — 25 tests (all mapping functions with edge cases)
    • provider.test.ts — 7 tests (validate, phases, filtering)
  • packages/icons/src/betterstack.tsx — Better Stack icon component
  • packages/importers/README.md — Package documentation with architecture, pipeline flow, and "adding a new provider" guide

Modified files (6)

  • packages/importers/src/index.ts — Register betterstack provider in IMPORT_PROVIDERS
  • packages/importers/package.json — Add ./betterstack and ./betterstack/fixtures export paths
  • packages/icons/src/index.tsx — Export Better Stack icon
  • packages/api/src/service/import.ts — Add writeMonitorsPhase(), createProvider() routing, monitor limit warnings, monitors/monitorGroups/sections phase cases
  • packages/api/src/router/import.ts — Accept "betterstack" provider, betterstackStatusPageId, includeMonitors option
  • apps/dashboard/src/components/forms/components/form-import.tsx — Better Stack radio button, provider-specific fields, monitors toggle

Test plan

  • bun test in packages/importers/ — 75 tests pass (40 new + 35 existing)
  • Manual test with a real Better Stack API token
  • Verify dashboard form renders both providers correctly
  • Test preview + import flow end-to-end against local Turso

Signed-off-by: Moulik Aggarwal <qwertymoulik@gmail.com>
Signed-off-by: Moulik Aggarwal <qwertymoulik@gmail.com>
@vercel
Copy link

vercel bot commented Mar 26, 2026

@aggmoulik is attempting to deploy a commit to the OpenStatus Team on Vercel.

A member of the Team first needs to authorize it.

Copy link

@cubic-dev-ai cubic-dev-ai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

2 issues found across 17 files

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="packages/importers/src/providers/betterstack/provider.ts">

<violation number="1" location="packages/importers/src/providers/betterstack/provider.ts:113">
P2: The monitor-groups fetch + mapping block is copy-pasted identically in both branches of the `if (statusPages.length > 0)` / `else`. Move it after the conditional to eliminate the duplication — `pageId` is already in scope.</violation>
</file>

<file name="packages/api/src/service/import.ts">

<violation number="1" location="packages/api/src/service/import.ts:41">
P2: Unknown provider names silently fall through to `createStatuspageProvider()`. Separate the `default` case to throw an error for unrecognized providers, so misconfigurations fail fast instead of producing confusing downstream errors.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

Signed-off-by: Moulik Aggarwal <qwertymoulik@gmail.com>
Signed-off-by: Moulik Aggarwal <qwertymoulik@gmail.com>
Signed-off-by: Moulik Aggarwal <qwertymoulik@gmail.com>
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds a new Better Stack Uptime importer to @openstatus/importers and wires it through the API import pipeline and Dashboard UI so users can preview/run imports (including monitors) from Better Stack into OpenStatus.

Changes:

  • Introduces a new Better Stack provider (Zod API types, client w/ pagination, mappers, orchestration, fixtures, and tests).
  • Extends the API import pipeline to support a betterstack provider, monitor imports, and monitor plan-limit warnings.
  • Updates Dashboard import form and icons to support Better Stack-specific inputs and toggles.

Reviewed changes

Copilot reviewed 17 out of 17 changed files in this pull request and generated 7 comments.

Show a summary per file
File Description
packages/importers/src/providers/betterstack/api-types.ts Zod schemas + TS types for Better Stack API resources.
packages/importers/src/providers/betterstack/client.ts Better Stack HTTP client (Bearer auth, JSON:API-style pagination).
packages/importers/src/providers/betterstack/client.test.ts Tests for Better Stack client (auth, pagination, errors, parsing).
packages/importers/src/providers/betterstack/fixtures.ts Shared mock payloads for Better Stack provider tests.
packages/importers/src/providers/betterstack/index.ts Barrel exports for the Better Stack provider package entry.
packages/importers/src/providers/betterstack/mapper.ts Mapping functions from Better Stack shapes to OpenStatus insert shapes.
packages/importers/src/providers/betterstack/mapper.test.ts Unit tests for all Better Stack mappers.
packages/importers/src/providers/betterstack/provider.ts Provider orchestration (validate + phase generation).
packages/importers/src/providers/betterstack/provider.test.ts Provider integration tests (phase structure, filtering, resources).
packages/importers/src/index.ts Registers betterstack in IMPORT_PROVIDERS and re-exports.
packages/importers/package.json Adds ./betterstack and ./betterstack/fixtures export paths.
packages/importers/README.md New package-level docs incl. provider architecture and phase mapping.
packages/icons/src/betterstack.tsx Adds Better Stack icon component.
packages/icons/src/index.tsx Exports the new Better Stack icon.
packages/api/src/service/import.ts Adds provider routing, monitor limit warnings, and monitor write phase.
packages/api/src/router/import.ts Accepts betterstack provider + Better Stack-specific inputs and options.
apps/dashboard/src/components/forms/components/form-import.tsx UI support for Better Stack selection, token help text, status page id, and monitor toggle.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +102 to +121
// 3. Monitor count limit
const monitorsPhase = summary.phases.find((p) => p.phase === "monitors");
if (monitorsPhase && monitorsPhase.resources.length > 0) {
const maxMonitors = config.limits.monitors;
const existingMonitors = await db
.select()
.from(monitor)
.where(eq(monitor.workspaceId, config.workspaceId))
.all();
const remaining = maxMonitors - existingMonitors.length;
if (remaining <= 0) {
summary.errors.push(
`Monitor limit reached (${maxMonitors}). Upgrade your plan to import monitors.`,
);
} else if (monitorsPhase.resources.length > remaining) {
summary.errors.push(
`Only ${remaining} of ${monitorsPhase.resources.length} monitors can be imported due to plan limit (${maxMonitors}).`,
);
}
}
Copy link

Copilot AI Mar 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new monitor limit warning logic in addLimitWarnings() isn't covered by the existing packages/api/src/service/import.test.ts suite (which already tests components/custom-domain/subscribers). Adding tests for the monitors phase warnings (under/over limit, existing monitors) would help prevent regressions.

Copilot uses AI. Check for mistakes.
Comment on lines 227 to 239
@@ -182,6 +237,18 @@ export async function runImport(config: {
phase.status = "skipped";
}
break;
Copy link

Copilot AI Mar 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When targetPageId is falsy (no page created and no pageId provided), the monitorGroups/componentGroups case currently does nothing unless includeComponents === false. That leaves the phase in its original "completed" state even though nothing was written. Explicitly mark the phase as skipped (and ideally update resource statuses) when targetPageId is missing.

Copilot uses AI. Check for mistakes.
Comment on lines +248 to +249
} else if (config.options?.includeComponents === false) {
phase.status = "skipped";
Copy link

Copilot AI Mar 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Similar to the monitorGroups case: if targetPageId is not set, the sections phase will silently do nothing (unless includeComponents === false), leaving the phase reported as completed. Set phase.status = "skipped" (and update resource statuses) when there is no page to attach groups/sections to.

Suggested change
} else if (config.options?.includeComponents === false) {
phase.status = "skipped";
} else if (
config.options?.includeComponents === false ||
!targetPageId
) {
phase.status = "skipped";
for (const resource of phase.resources) {
resource.status = "skipped";
}

Copilot uses AI. Check for mistakes.
Comment on lines +100 to +126
// Monitor Groups → Component Groups (always fetched regardless of status pages)
const monitorGroups = await client.getMonitorGroups();
const groupResources: ResourceResult[] = monitorGroups.map((g) => ({
sourceId: g.id,
name: g.attributes.name,
status: "created" as const,
data: mapMonitorGroup(g, config.workspaceId, pageId),
}));
phases.push({
phase: "monitorGroups",
status: "completed",
resources: groupResources,
});

// Phase 5: Incidents → Status Reports
const incidents = await client.getIncidents();
const incidentResources: ResourceResult[] = incidents.map((inc) => ({
sourceId: inc.id,
name: inc.attributes.name ?? `Incident ${inc.id}`,
status: "created" as const,
data: mapIncidentToStatusReport(inc, config.workspaceId, pageId),
}));
phases.push({
phase: "incidents",
status: "completed",
resources: incidentResources,
});
Copy link

Copilot AI Mar 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

run() always emits an incidents phase even when no page context exists (config.pageId is undefined and betterstackStatusPageId filtering yields no status page). In that situation the service layer won't be able to write incidents, but the phase will still be reported as completed. Consider only emitting page-scoped phases (sections/monitorGroups/incidents) when you have a page to attach to, or mark them as skipped with a clear reason in the summary.

Copilot uses AI. Check for mistakes.
const maxMonitors = limits.monitors;
const remaining = maxMonitors - existingMonitors.length;

if (remaining <= 0) {
Copy link

Copilot AI Mar 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If remaining <= 0, writeMonitorsPhase sets phase.status = "failed" and returns, but it does not update individual resource.status values. Since importer providers initially mark resources as created, this can leave the summary claiming monitors were created when none were written. Mark all monitor resources as skipped/failed (with an explanatory resource.error) before returning so the summary accurately reflects what happened.

Suggested change
if (remaining <= 0) {
if (remaining <= 0) {
for (const resource of phase.resources) {
resource.status = "skipped";
resource.error = `Skipped: monitor limit reached (max ${maxMonitors})`;
}

Copilot uses AI. Check for mistakes.
Comment on lines +24 to +33
validate: async (config) => {
try {
const client = createBetterstackClient(config.apiKey);
await client.getMonitors();
return { valid: true };
} catch (err) {
return {
valid: false,
error: err instanceof Error ? err.message : String(err),
};
Copy link

Copilot AI Mar 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

validate() currently calls client.getMonitors(), which fetches all monitor pages due to requestAllPages(). That can be very slow/expensive for accounts with many monitors and makes validation heavier than necessary. Consider validating with a single lightweight request (e.g., fetch the first page only / a dedicated "me" endpoint / limit=1 style request) so validation only checks auth/permissions without pulling full datasets.

Copilot uses AI. Check for mistakes.
Comment on lines +103 to +111
const monitorsPhase = summary.phases.find((p) => p.phase === "monitors");
if (monitorsPhase && monitorsPhase.resources.length > 0) {
const maxMonitors = config.limits.monitors;
const existingMonitors = await db
.select()
.from(monitor)
.where(eq(monitor.workspaceId, config.workspaceId))
.all();
const remaining = maxMonitors - existingMonitors.length;
Copy link

Copilot AI Mar 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In addLimitWarnings(), the monitor limit check loads all existing monitors with .select().from(monitor)...all() just to compute a count. For larger workspaces this is unnecessarily heavy; prefer a COUNT(*) (or a minimal select({ id: monitor.id })) so the warning check stays cheap.

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants