Skip to content

Migration to OpenStatus from Atlassian StatusPage - #1933

Merged
mxkaske merged 20 commits into
openstatusHQ:mainfrom
aggmoulik:feat/statuspage-importer
Mar 12, 2026
Merged

mxkaske merged 20 commits into
openstatusHQ:mainfrom
aggmoulik:feat/statuspage-importer

Conversation

@aggmoulik

Copy link
Copy Markdown
Collaborator

No description provided.

aggmoulik and others added 5 commits March 5, 2026 20:46
…, and fixtures

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…gration

Add DB write service with phased transactions and per-resource error handling,
tRPC router with preview/run endpoints, and register on the lambda router.
Also enrich provider output with sourceGroupId on components and
sourceComponentIds on maintenances for proper relational linking.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@vercel

vercel Bot commented Mar 5, 2026

Copy link
Copy Markdown

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

A member of the Team first needs to authorize it.

@mxkaske mxkaske left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

make sure to align with the current dependencies (and lets fix the versions)

Comment thread packages/importers/package.json
@mxkaske
mxkaske requested a review from Copilot March 5, 2026 19:54

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

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 @openstatus/importers package with a Statuspage (Atlassian) importer and wires it into the API to preview and execute migrations into OpenStatus.

Changes:

  • Introduces a Statuspage client, API type schemas, mapping utilities, and provider implementation (with Bun tests/fixtures).
  • Adds API service + TRPC router endpoints to preview and run imports, including phase-by-phase DB writes and idempotency.
  • Registers the new router in the API lambda entrypoint and adds the importers workspace dependency.

Reviewed changes

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

Show a summary per file
File Description
packages/importers/tsconfig.json Creates a TS build config for the new importers package.
packages/importers/package.json Defines the new @openstatus/importers package exports and scripts.
packages/importers/src/types.ts Adds shared import/provider result and config types.
packages/importers/src/index.ts Exposes public API and a provider registry.
packages/importers/src/providers/statuspage/api-types.ts Adds Zod schemas/types for Statuspage API payloads.
packages/importers/src/providers/statuspage/client.ts Implements a typed Statuspage API client.
packages/importers/src/providers/statuspage/mapper.ts Maps Statuspage entities into OpenStatus-ready shapes.
packages/importers/src/providers/statuspage/provider.ts Orchestrates fetching + mapping into phased import summaries.
packages/importers/src/providers/statuspage/index.ts Exports Statuspage provider/client/types.
packages/importers/src/providers/statuspage/fixtures.ts Adds fixtures for provider/client/mapper tests.
packages/importers/src/providers/statuspage/*.test.ts Adds Bun tests for client, mapper, and provider.
packages/api/package.json Adds @openstatus/importers workspace dependency.
packages/api/src/service/import.ts Implements preview/run import flows and DB phase writers.
packages/api/src/router/import.ts Adds TRPC endpoints for previewing and running an import.
packages/api/src/router/import.test.ts Adds integration-style tests for preview/run + idempotency.
packages/api/src/lambda.ts Registers the new importRouter.
Files not reviewed (1)
  • pnpm-lock.yaml: Language not supported

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

Comment thread packages/api/src/router/import.ts
Comment thread packages/api/src/router/import.ts
Comment thread packages/api/src/service/import.ts
Comment thread packages/importers/src/providers/statuspage/mapper.ts Outdated
Comment thread packages/importers/src/providers/statuspage/provider.ts Outdated
Comment thread packages/importers/src/providers/statuspage/client.ts Outdated
Comment thread packages/importers/src/providers/statuspage/client.ts Outdated
Signed-off-by: Moulik Aggarwal <qwertymoulik@gmail.com>
Signed-off-by: Moulik Aggarwal <qwertymoulik@gmail.com>
@aggmoulik

Copy link
Copy Markdown
Collaborator Author

@claude review

@openstatusHQ openstatusHQ deleted a comment from claude Bot Mar 6, 2026
@mxkaske

mxkaske commented Mar 6, 2026

Copy link
Copy Markdown
Member

@claude review

@claude

claude Bot commented Mar 6, 2026

Copy link
Copy Markdown

Claude Code is working…

I'll analyze this and get back to you.

View job run

@mxkaske
mxkaske requested a review from Copilot March 6, 2026 09:25
@vercel

vercel Bot commented Mar 6, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
openstatus-dashboard Ready Ready Preview, Comment Mar 8, 2026 2:34pm
openstatus-status-page Ready Ready Preview, Comment Mar 8, 2026 2:34pm
openstatus-web Ready Ready Preview, Comment Mar 8, 2026 2:34pm

Request Review

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 18 out of 19 changed files in this pull request and generated 9 comments.

Files not reviewed (1)
  • pnpm-lock.yaml: Language not supported

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

Comment on lines +67 to +86
async getIncidents(pageId: string): Promise<StatuspageIncident[]> {
return this.request(
`/pages/${pageId}/incidents`,
z.array(StatuspageIncidentSchema),
);
}

async getScheduledIncidents(pageId: string): Promise<StatuspageIncident[]> {
return this.request(
`/pages/${pageId}/incidents/scheduled`,
z.array(StatuspageIncidentSchema),
);
}

async getSubscribers(pageId: string): Promise<StatuspageSubscriber[]> {
return this.request(
`/pages/${pageId}/subscribers`,
z.array(StatuspageSubscriberSchema),
);
}

Copilot AI Mar 6, 2026

Copy link

Choose a reason for hiding this comment

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

The Statuspage API for /pages/{page_id}/incidents and /pages/{page_id}/subscribers returns paginated results (defaulting to 25 items per page). The current getIncidents and getSubscribers client methods only fetch the first page, so organizations with more than 25 incidents or subscribers will have their data silently truncated. The client should implement pagination (e.g., looping through pages until all results are collected) for these endpoints.

Copilot uses AI. Check for mistakes.
Comment on lines +8 to +9
"./statuspage": "./src/providers/statuspage/index.ts",
"./statuspage/fixtures": "./src/providers/statuspage/fixtures.ts"

Copilot AI Mar 6, 2026

Copy link

Choose a reason for hiding this comment

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

The fixtures.ts file is exposed as a public package entry point ("./statuspage/fixtures": "./src/providers/statuspage/fixtures.ts") in package.json. While this is useful for sharing test fixtures across packages, exporting test-specific data from the production package creates a code smell. Consider moving it to a dedicated test utilities package or a separate export namespace that signals it is not production API surface.

Suggested change
"./statuspage": "./src/providers/statuspage/index.ts",
"./statuspage/fixtures": "./src/providers/statuspage/fixtures.ts"
"./statuspage": "./src/providers/statuspage/index.ts"

Copilot uses AI. Check for mistakes.
Comment on lines +154 to +159
case "webhook":
return {
email: "webhook@imported.openstatus.dev",
pageId,
channelType: "webhook",
webhookUrl: subscriber.endpoint ?? "",

Copilot AI Mar 6, 2026

Copy link

Choose a reason for hiding this comment

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

The mapSubscriber function sets email: "webhook@imported.openstatus.dev" for webhook subscribers (line 156). However, the pageSubscriber database schema enforces a CHECK constraint requiring email IS NULL when channelType = 'webhook'. This placeholder email will cause a constraint violation when the subscriber is inserted. The email field should be null for webhook subscribers (and the return type should reflect this). The hardcoded value was likely intended as a placeholder but it conflicts with the database-level constraint.

Copilot uses AI. Check for mistakes.
Comment on lines +234 to +244
const existingBySlug = await db
.select()
.from(page)
.where(eq(page.slug, data.slug))
.get();

if (existingBySlug) {
resource.openstatusId = existingBySlug.id;
resource.status = "skipped";
phase.status = "completed";
return existingBySlug.id;

Copilot AI Mar 6, 2026

Copy link

Choose a reason for hiding this comment

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

The slug-based idempotency check in writePagePhase (lines 234–244) queries for a page by data.slug without also filtering by workspaceId. Since page.slug is globally unique across all workspaces, this could return a page belonging to a different workspace. If another workspace already has a page with the same slug as the one being imported (e.g., a common subdomain), the import would silently use that foreign page as the target and write components, incidents, and subscribers into it. The query should additionally filter by eq(page.workspaceId, workspaceId) to scope the lookup to the current workspace only.

Copilot uses AI. Check for mistakes.
Comment on lines +430 to +486
// Insert status report
const [insertedReport] = await db
.insert(statusReport)
.values({
title: data.report.title,
status: data.report.status,
workspaceId,
pageId,
})
.returning({ id: statusReport.id });

if (!insertedReport) {
resource.status = "failed";
resource.error = "Failed to insert status report";
continue;
}

// Insert status report updates
if (data.updates.length > 0) {
await db.insert(statusReportUpdate).values(
data.updates.map((u) => ({
status: u.status,
message: u.message,
date: u.date,
statusReportId: insertedReport.id,
})),
);
}

// Link to page components
const componentLinks: Array<{
statusReportId: number;
pageComponentId: number;
}> = [];
for (const sourceCompId of data.sourceComponentIds) {
const osCompId = componentIdMap.get(sourceCompId);
if (osCompId) {
componentLinks.push({
statusReportId: insertedReport.id,
pageComponentId: osCompId,
});
}
}
if (componentLinks.length > 0) {
await db.insert(statusReportsToPageComponents).values(componentLinks);
}

resource.openstatusId = insertedReport.id;
resource.status = "created";
} catch (err) {
resource.status = "failed";
resource.error = err instanceof Error ? err.message : String(err);
}
}

phase.status = computePhaseStatus(phase.resources);
}

Copilot AI Mar 6, 2026

Copy link

Choose a reason for hiding this comment

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

The writeIncidentsPhase and writeMaintenancesPhase functions have no idempotency check — they always insert a new status report or maintenance record without checking whether one with the same title and pageId already exists. Re-running the import (which is supported as shown in the idempotency test) will create duplicate status reports and maintenance records. An idempotency check (e.g., by title + pageId) should be added, similar to how writeComponentsPhase and writeComponentGroupsPhase check by name + pageId.

Copilot uses AI. Check for mistakes.
Comment on lines +12 to +23
provider: z.enum(["statuspage"]),
apiKey: z.string().min(1),
statuspagePageId: z.string().optional(),
}),
)
.query(async (opts) => {
return previewImport({
apiKey: opts.input.apiKey,
statuspagePageId: opts.input.statuspagePageId,
workspaceId: opts.ctx.workspace.id,
});
}),

Copilot AI Mar 6, 2026

Copy link

Choose a reason for hiding this comment

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

The provider input field is accepted and validated by both preview and run procedures (enforcing z.enum(["statuspage"])), but it is never actually used to dispatch to the correct provider — both procedures always instantiate StatuspageImportProvider directly in the service layer. As more providers are added to IMPORT_PROVIDERS, callers could pass provider: "someOtherProvider" and the system would silently run the Statuspage importer instead. The provider input should be forwarded to the service and used to select the correct provider implementation.

Copilot uses AI. Check for mistakes.
Comment thread packages/api/src/service/import.ts
Comment thread packages/api/src/service/import.ts Outdated
Comment thread packages/api/src/service/import.ts Outdated
Comment on lines +624 to +631
const [inserted] = await db
.insert(pageSubscriber)
.values({
email: data.email,
pageId,
channelType: "webhook",
webhookUrl: data.webhookUrl,
})

Copilot AI Mar 6, 2026

Copy link

Choose a reason for hiding this comment

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

In writeSubscribersPhase, when inserting a webhook subscriber (lines 624–631), the code sets email: data.email where data.email is the hardcoded string "webhook@imported.openstatus.dev" from the mapper. However, the pageSubscriber table has a CHECK constraint that requires email to be NULL for webhook channel type (the constraint is: channelType = 'webhook' AND webhookUrl IS NOT NULL AND email IS NULL). Passing a non-null email for a webhook subscriber will violate this constraint and cause a database error at runtime. The email field should be set to null (or omitted) when inserting webhook subscribers.

Copilot uses AI. Check for mistakes.
Comment thread packages/importers/src/providers/statuspage/client.ts
Comment thread packages/importers/src/providers/statuspage/provider.ts
mxkaske and others added 4 commits March 8, 2026 12:11
…assertion

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Resources marked as "skipped" by the component limit truncation were
being processed and overwritten by writeComponentsPhase.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Create a fresh page instead of relying on seeded pageId: 1 to ensure
predictable DB state for the component limit enforcement test.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Create a fresh page instead of relying on seeded pageId: 1 to ensure
predictable DB state for the component limit enforcement test.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@mxkaske

mxkaske commented Mar 8, 2026

Copy link
Copy Markdown
Member

@claude review

@claude

claude Bot commented Mar 8, 2026 •

Copy link
Copy Markdown

Claude finished @mxkaske's task in 2m 26s —— View job


Code Review: StatusPage Importer

  • Gather context
  • Read key files
  • Analyze for bugs and issues
  • Post review

Great feature addition overall! The architecture is clean and the phased import approach with idempotency is solid. Several issues need to be addressed before merging though.

Critical Issues

1. No idempotency for incidents/maintenances (packages/api/src/service/import.ts:553, 629)

writeIncidentsPhase and writeMaintenancesPhase always insert without checking for existing records. Re-running the import creates duplicates. Components and groups have idempotency checks but incidents/maintenances don't. Need a check like eq(statusReport.title, data.report.title), eq(statusReport.pageId, pageId) similar to what writeComponentsPhase does. Fix this →

2. getScheduledIncidents is defined but never called (packages/importers/src/providers/statuspage/client.ts:80, provider.ts:53-58)

The client has both getIncidents and getScheduledIncidents, but the provider only calls getIncidents and then filters locally. The Statuspage API /incidents endpoint returns only unresolved/active incidents — historical resolved incidents may not be included. If the intent is to import all historical incidents, you likely need to hit a separate endpoint (e.g. with ?limit= or the dedicated scheduled endpoint for maintenances). Either use getScheduledIncidents for maintenances or document the limitation.

3. apiKey sent as a query parameter (packages/api/src/router/import.ts:9-26, form-import.tsx:89-102)

The preview endpoint is a query (tRPC GET request), which means the apiKey is included in the URL and will appear in server logs, access logs, and browser history. This should be a mutation (POST) to keep the API key in the request body. Fix this →

Design Issues

4. provider input field is ignored (packages/api/src/router/import.ts:12, 31)

Both preview and run accept provider: z.enum(["statuspage"]) but the service layer always creates createStatuspageProvider() directly. As new providers are added to IMPORT_PROVIDERS, callers passing provider: "newProvider" would silently run the Statuspage importer. The field should be forwarded and used for dispatch.

5. Double getPages() calls in previewImport (packages/api/src/service/import.ts:108-119)

provider.validate() calls client.getPages(), then provider.run() calls it again immediately after. The pages result should be passed through from validate to run to avoid the redundant network call.

6. config.pageId ?? 0 sentinel value (packages/importers/src/providers/statuspage/provider.ts:76)

Using 0 as a sentinel for "no page in dry-run" leaks into all mapped resources and confuses API consumers. As the copilot review suggests, pageId in mapper outputs should be number | undefined and omitted during preview rather than set to an invalid sentinel.

7. Router naming inconsistency (packages/api/src/edge.ts:46)

importRouter: importRouter,  // inconsistent

All other routers follow workspace: workspaceRouter, monitor: monitorRouter naming. This should be import: importRouter (object key can use reserved words). This also affects how the frontend references it — currently trpc.importRouter.preview instead of the more idiomatic trpc.import.preview. Fix this →

Minor Issues

8. mapIncidentToMaintenance null safety (packages/importers/src/providers/statuspage/mapper.ts:130)

from: new Date(incident.scheduled_for as string),  // cast, not guaranteed safe

The isScheduledIncident guard ensures scheduled_for != null before this is called, so it's safe in practice — but the type cast is a code smell. Prefer an explicit null check + throw (as suggested in the copilot review) for clarity.

9. fixtures.ts is a public package export (packages/importers/package.json:10)

Test fixtures shouldn't be part of the production package API surface. Remove "./statuspage/fixtures" from exports and use internal imports in tests. Fix this →

10. TypeScript version uses caret (packages/importers/package.json:22)

Per @mxkaske's suggestion: "typescript": "^5.9.3" should be "typescript": "5.9.3" (no caret, matching pinned versions in other packages).

11. Form default value type mismatch (apps/dashboard/src/components/forms/components/form-import.tsx:75)

defaultValues: {
  provider: undefined,  // type is z.enum(["statuspage"]), undefined is not valid

12. Typo (apps/dashboard/src/components/forms/components/form-import.tsx:233)

"Import a specific page. Leave empty to import accross pages." → across

13. Maintenance skipped by includeIncidents: false (packages/api/src/service/import.ts:237, 244)

The maintenances phase is gated on includeIncidents !== false. As copilot noted, maintenances (scheduled events) are logically distinct from incidents (unplanned outages). Consider a dedicated includeMaintenances option, or at least update the UI label to make it clear that toggling "Status Reports" also controls maintenances (which the current UI already does by calling the toggle "Status Reports & Maintenances", so the UI is ahead of the code here).


Summary

The overall structure is well thought out. Must-fix before merging: idempotency for incidents/maintenances, the API key security issue with GET requests, and the unused getScheduledIncidents clarification. The design issues (provider dispatch, double API calls, sentinel pageId) would be good to clean up as well.

@mxkaske
mxkaske merged commit 76e68b1 into openstatusHQ:main Mar 12, 2026
8 of 9 checks passed

This branch was successfully deployed

3 active deployments
Preview – openstatus-dashboard — 1b63cc6e Deployed Mar 12, 2026 by vercel[bot]
Preview – openstatus-status-page — 1b63cc6e Deployed Mar 12, 2026 by vercel[bot]
Preview – openstatus-web — 1b63cc6e Deployed Mar 12, 2026 by vercel[bot]
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.

4 participants