Skip to content

feat: add Microsoft Teams notification provider#261

Open
mvanhorn wants to merge 3 commits intoPortabase:mainfrom
mvanhorn:feat/260-teams-notification
Open

feat: add Microsoft Teams notification provider#261
mvanhorn wants to merge 3 commits intoPortabase:mainfrom
mvanhorn:feat/260-teams-notification

Conversation

@mvanhorn
Copy link
Copy Markdown

@mvanhorn mvanhorn commented Apr 10, 2026

Summary

Adds Microsoft Teams as a notification provider. Uses the Teams Incoming Webhook with Adaptive Cards format, following the same pattern as the existing Slack provider.

Changes

  • Created src/features/notifications/providers/teams.ts with sendTeams() function
  • Added 'teams' to ProviderKind union in types.ts
  • Registered sendTeams handler in providers/index.ts

The Adaptive Card payload includes the alert level (bold heading), message body (with word wrap), and optional data block (monospace).

Fixes #260

This contribution was developed with AI assistance (Codex).

Summary by CodeRabbit

  • New Features
    • Added Microsoft Teams notification provider support. Users can now configure the system to send event notifications directly to Microsoft Teams channels via webhook integration. Notifications are delivered as adaptive cards containing the event level, title, message content, and any additional associated data, with full error handling for delivery failures.

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Apr 10, 2026

Important

Review skipped

Review was skipped due to path filters

⛔ Files ignored due to path filters (3)
  • docs/screenshots/teams-create-form.png is excluded by !**/*.png
  • docs/screenshots/teams-edit-form.png is excluded by !**/*.png
  • docs/screenshots/teams-provider-picker.png is excluded by !**/*.png

CodeRabbit blocks several paths by default. You can override this behavior by explicitly including those paths in the path filters. For example, including **/dist/** will override the default block on the dist directory, by removing the pattern from both the lists.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 5e45c0a6-2e6b-4d8e-b5e3-f82af8475d41

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds Microsoft Teams as a notification provider: new Teams provider module, registration in provider handlers, type and DB enum updates, UI form/schema for Teams channel, and corresponding DB migration and snapshot files.

Changes

Cohort / File(s) Summary
Provider implementation & registration
src/features/notifications/providers/teams.ts, src/features/notifications/providers/index.ts
New sendTeams export that builds an Adaptive Card payload, POSTs to a Teams webhook with timeout and error handling; registered under microsoft-teams in the handlers map.
Types & DB enum
src/features/notifications/types.ts, src/db/schema/09_notification-channel.ts, src/db/migrations/0049_mixed_blur.sql
Added 'microsoft-teams' to the ProviderKind TypeScript union and to the PostgreSQL provider_kind enum via migration.
UI: form + schema + integration
src/components/wrappers/.../providers/notifications/forms/teams.form.tsx, src/components/wrappers/.../providers/notifications/forms/teams.schema.ts, src/components/wrappers/.../channel-form/channel-form.schema.ts, src/components/wrappers/.../helpers/common.tsx, src/components/wrappers/.../helpers/notification.tsx
New Teams channel config Zod schema and React form component; integrated into channel form schema, form renderer (renderChannelForm), and provider registry (removed preview: true).
Database migration metadata / snapshot
src/db/migrations/meta/0049_snapshot.json, src/db/migrations/meta/_journal.json
Added migration snapshot and journal entry for migration 0049 (large snapshot reflecting enum and schema state).

Sequence Diagram(s)

sequenceDiagram
    participant App as Notification System
    participant Handler as sendTeams
    participant API as Teams Webhook API

    App->>Handler: sendTeams(config{teamsWebhook}, payload)
    Handler->>Handler: Build Adaptive Card JSON\n(include level, title, message, optional data via safeStringify)
    Handler->>API: POST webhook (application/json) with 10s timeout
    alt 2xx response
        API-->>Handler: 2xx + body
        Handler-->>App: Return DispatchResult(success)
    else non-2xx response
        API-->>Handler: status + body
        Handler->>Handler: Read body text
        Handler-->>App: Throw Error(status + body)
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Poem

🐰 I nibble code beneath the moonlight,

Cards and webhooks springing bright,
Teams now listens to my hop and cheer,
Messages bound to channels near,
Hooray — notifications delivered, clear! 🎉

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Linked Issues check ⚠️ Warning All coding objectives from issue #260 are met: sendTeams implementation, provider registration, validation schema, form component, channel form schema integration, provider registry update, and form renderer integration are all complete. The PR fails to include mandatory validation screenshots demonstrating the UI functionality as explicitly required by issue #260. Add screenshots of the notification channel dialog, create mode form, and edit mode form before merge.
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately and concisely summarizes the main change: adding Microsoft Teams as a notification provider.
Out of Scope Changes check ✅ Passed All changes are directly related to implementing the Microsoft Teams notification provider as specified in issue #260. Database migrations and schema updates are necessary in-scope additions supporting the feature.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/features/notifications/providers/teams.ts`:
- Around line 30-35: The current Teams notification payload builds a TextBlock
using JSON.stringify(payload.data, null, 2) which can throw for circular or
non-serializable objects; in the payload construction (the spread that begins
with ...(payload.data ? [{ type: 'TextBlock', text: `Data:
${JSON.stringify(payload.data, null, 2)}`, fontType: 'monospace' }]) ), wrap the
serialization in a safe routine: try to JSON.stringify(payload.data, null, 2)
inside a try/catch and on error fall back to a non-throwing representation (e.g.
a simple string like "[unserializable data]" or use util.inspect/safeStringify),
then use that string in the TextBlock so the notification is still sent even
when payload.data cannot be serialized.
- Around line 46-50: The fetch call that posts to Teams (the const res = await
fetch(webhookUrl, {...}) in teams.ts) needs an AbortController timeout so slow
webhooks don't hang the process; create an AbortController, start a timer (e.g.,
setTimeout) to call controller.abort() after a configured timeout (e.g., 5–10s
or existing notification timeout), pass controller.signal into the fetch
options, and clear the timer after fetch completes or throws; apply the same
AbortController+timeout pattern to the other providers (webhook.ts, discord.ts,
slack.ts, telegram.ts, ntfy.ts, gotify.ts) where fetch is used.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: d361f553-ebbd-41a8-a71c-43e7550c741d

📥 Commits

Reviewing files that changed from the base of the PR and between a620d7a and 0e25e5e.

📒 Files selected for processing (3)
  • src/features/notifications/providers/index.ts
  • src/features/notifications/providers/teams.ts
  • src/features/notifications/types.ts
📜 Review details
🔇 Additional comments (3)
src/features/notifications/providers/index.ts (1)

4-17: Teams provider registration is correctly wired.

Import and handler-map registration are consistent with ProviderKind and the dispatcher flow.

src/features/notifications/types.ts (1)

1-1: ProviderKind extension is correct.

Adding 'teams' keeps provider typing aligned across dispatcher and provider implementations.

src/features/notifications/providers/teams.ts (1)

57-62: Success DispatchResult shape looks consistent.

Return fields align with provider conventions and DispatchResult contract.

@KillianLarcher
Copy link
Copy Markdown
Collaborator

Hi @mvanhorn,

Thanks for your contribution.

However, this PR is missing important features, mainly on the UI side. It also appears to have been opened just 12 minutes after the “First Good Issue” was created, which suggests it may have been fully generated without human review.

If you still want this PR to be considered for review and validation, please improve the Microsoft Teams integration and ensure a proper manual review of the implementation before resubmitting. Please also add the required screenshots as specified in the issue; the PR cannot be validated without them.

- Add Teams webhook URL validation schema
- Add Teams configuration form component (matching existing Discord pattern)
- Register Teams in channel form schema, provider registry, and form renderer
- Add DB migration to extend provider_kind enum with 'microsoft-teams'
- Address CodeRabbit review: safe JSON.stringify for circular refs, AbortController timeout on fetch
- Remove preview flag to make Teams a fully available provider
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/features/notifications/providers/teams.ts`:
- Around line 41-50: The conditional that decides whether to include the Data
TextBlock incorrectly uses truthiness on payload.data (so values like 0, false,
and "" are dropped); update the check in the teams notification builder to test
for null/undefined instead (e.g., payload.data != null or payload.data !==
undefined) so the block is included when payload.data is any falsy-but-present
value before calling safeStringify(payload.data).
- Around line 3-11: The safeStringify function currently throws on bigint
values; update the JSON.stringify replacer inside safeStringify (the replacer
used in safeStringify) to detect typeof value === 'bigint' and return
value.toString(), keep the existing circular-detection logic with seen
(WeakSet), and add a catch-all fallback in the replacer (e.g., return
String(value) for any value that would otherwise cause serialization errors) so
JSON.stringify never throws on unsupported primitives; preserve the 2-space
formatting.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: aef5bb84-a449-4d89-8fa7-878d018f0c3a

📥 Commits

Reviewing files that changed from the base of the PR and between 0e25e5e and b366468.

📒 Files selected for processing (12)
  • src/components/wrappers/dashboard/admin/channels/channel/channel-form/channel-form.schema.ts
  • src/components/wrappers/dashboard/admin/channels/channel/channel-form/providers/notifications/forms/teams.form.tsx
  • src/components/wrappers/dashboard/admin/channels/channel/channel-form/providers/notifications/forms/teams.schema.ts
  • src/components/wrappers/dashboard/admin/channels/helpers/common.tsx
  • src/components/wrappers/dashboard/admin/channels/helpers/notification.tsx
  • src/db/migrations/0049_mixed_blur.sql
  • src/db/migrations/meta/0049_snapshot.json
  • src/db/migrations/meta/_journal.json
  • src/db/schema/09_notification-channel.ts
  • src/features/notifications/providers/index.ts
  • src/features/notifications/providers/teams.ts
  • src/features/notifications/types.ts
📜 Review details
🔇 Additional comments (12)
src/db/migrations/meta/_journal.json (1)

347-354: Migration journal append looks correct.

The new entry is sequential (idx: 49) and consistent with existing journal metadata shape.

src/db/migrations/0049_mixed_blur.sql (1)

1-1: Enum migration change is aligned with provider rollout.

Adding 'microsoft-teams' to provider_kind is the expected DB-side update for this feature.

src/features/notifications/providers/index.ts (1)

4-4: Teams provider registration is correctly wired.

sendTeams is imported and mapped to "microsoft-teams" in handlers, enabling dispatch routing as intended.

Also applies to: 17-17

src/components/wrappers/dashboard/admin/channels/helpers/common.tsx (1)

23-25: Teams form rendering path is implemented correctly.

The Teams form component is imported and returned in renderChannelForm for "microsoft-teams".

Also applies to: 93-94

src/components/wrappers/dashboard/admin/channels/channel/channel-form/providers/notifications/forms/teams.schema.ts (1)

3-5: Teams config schema is clean and sufficient for this scope.

teamsWebhook URL validation is present and matches expected form/provider integration.

src/features/notifications/types.ts (1)

1-1: ProviderKind extension is consistent with the new provider integration.

Including 'microsoft-teams' keeps dispatch typing aligned across feature layers.

src/components/wrappers/dashboard/admin/channels/helpers/notification.tsx (1)

16-16: Provider registry update correctly enables Teams.

Removing preview status and keeping the Teams entry in notificationProviders matches the rollout intent.

src/db/schema/09_notification-channel.ts (1)

10-10: DB schema enum update is correctly aligned.

Adding 'microsoft-teams' to providerKindEnum keeps persistence types in sync with the new provider.

src/components/wrappers/dashboard/admin/channels/channel/channel-form/channel-form.schema.ts (1)

9-9: LGTM.

The new discriminated-union arm keeps the form schema aligned with the Teams config schema and the provider key used elsewhere in the PR.

Also applies to: 52-55

src/components/wrappers/dashboard/admin/channels/channel/channel-form/providers/notifications/forms/teams.form.tsx (1)

20-35: LGTM.

The field binding is correct, and using PasswordInput here avoids exposing the Teams webhook by default.

src/features/notifications/providers/teams.ts (1)

57-80: Timeout handling looks good.

The AbortController plus finally cleanup closes the hanging-request gap without leaking the timer.

src/db/migrations/meta/0049_snapshot.json (1)

2436-2448: LGTM.

The generated snapshot includes the new microsoft-teams enum value, which keeps the migration metadata aligned with the runtime/provider changes.

Comment on lines +3 to +11
function safeStringify(data: Record<string, any>): string {
const seen = new WeakSet();
return JSON.stringify(data, (_key, value) => {
if (typeof value === 'object' && value !== null) {
if (seen.has(value)) return '[Circular]';
seen.add(value);
}
return value;
}, 2);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
node <<'NODE'
try {
  console.log(JSON.stringify({ id: 1n }, (_key, value) => value, 2));
} catch (error) {
  console.error(String(error));
}
NODE

Repository: Portabase/portabase

Length of output: 111


🏁 Script executed:

cat -n src/features/notifications/providers/teams.ts | head -30

Repository: Portabase/portabase

Length of output: 1233


🏁 Script executed:

#!/bin/bash
node <<'NODE'
// Test the proposed fix
const seen = new WeakSet();
function safeStringify(data) {
    try {
        return JSON.stringify(
            data,
            (_key, value) => {
                if (typeof value === 'bigint') return value.toString();
                if (typeof value === 'object' && value !== null) {
                    if (seen.has(value)) return '[Circular]';
                    seen.add(value);
                }
                return value;
            },
            2
        ) ?? '[unserializable payload data]';
    } catch {
        return '[unserializable payload data]';
    }
}

// Test with bigint
console.log("Test 1 - bigint value:");
console.log(safeStringify({ id: 1n }));

// Test with circular reference
console.log("\nTest 2 - circular reference:");
const obj = { a: 1 };
obj.self = obj;
console.log(safeStringify(obj));

// Test with normal data
console.log("\nTest 3 - normal data:");
console.log(safeStringify({ id: 123, name: "test" }));

// Test with mixed types
console.log("\nTest 4 - mixed types:");
console.log(safeStringify({ id: 456n, count: 10, data: { nested: 789n } }));
NODE

Repository: Portabase/portabase

Length of output: 318


Handle bigint serialization in safeStringify.

The function throws TypeError: Do not know how to serialize a BigInt when the payload contains bigint values. Convert bigint to string in the replacer and add a catch-all fallback.

Proposed fix
-function safeStringify(data: Record<string, any>): string {
-    const seen = new WeakSet();
-    return JSON.stringify(data, (_key, value) => {
-        if (typeof value === 'object' && value !== null) {
-            if (seen.has(value)) return '[Circular]';
-            seen.add(value);
-        }
-        return value;
-    }, 2);
+function safeStringify(data: unknown): string {
+    const seen = new WeakSet<object>();
+
+    try {
+        return JSON.stringify(
+            data,
+            (_key, value) => {
+                if (typeof value === 'bigint') return value.toString();
+                if (typeof value === 'object' && value !== null) {
+                    if (seen.has(value)) return '[Circular]';
+                    seen.add(value);
+                }
+                return value;
+            },
+            2
+        ) ?? '[unserializable payload data]';
+    } catch {
+        return '[unserializable payload data]';
+    }
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/features/notifications/providers/teams.ts` around lines 3 - 11, The
safeStringify function currently throws on bigint values; update the
JSON.stringify replacer inside safeStringify (the replacer used in
safeStringify) to detect typeof value === 'bigint' and return value.toString(),
keep the existing circular-detection logic with seen (WeakSet), and add a
catch-all fallback in the replacer (e.g., return String(value) for any value
that would otherwise cause serialization errors) so JSON.stringify never throws
on unsupported primitives; preserve the 2-space formatting.

Comment on lines +41 to +50
...(payload.data
? [
{
type: 'TextBlock',
text: `Data: ${safeStringify(payload.data)}`,
fontType: 'monospace',
wrap: true,
},
]
: []),
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Don't drop falsy payload.data values.

Line 41 gates the Data: block on truthiness, so 0, false, and "" disappear even when they are the payload. Check for null/undefined instead.

💡 Proposed fix
-                        ...(payload.data
+                        ...(payload.data !== undefined && payload.data !== null
                             ? [
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/features/notifications/providers/teams.ts` around lines 41 - 50, The
conditional that decides whether to include the Data TextBlock incorrectly uses
truthiness on payload.data (so values like 0, false, and "" are dropped); update
the check in the teams notification builder to test for null/undefined instead
(e.g., payload.data != null or payload.data !== undefined) so the block is
included when payload.data is any falsy-but-present value before calling
safeStringify(payload.data).

@mvanhorn
Copy link
Copy Markdown
Author

Thanks for the detailed feedback, @KillianLarcher. You were right that the initial submission was incomplete. I've now added the full implementation:

What's new in this push:

  • teams.schema.ts - Zod validation schema for webhook URL
  • teams.form.tsx - React form component (follows the existing Discord/Slack pattern)
  • channel-form.schema.ts - Teams added to discriminated union
  • common.tsx - Teams case in renderChannelForm
  • notification.tsx - Removed preview flag
  • DB migration (0049) to extend provider_kind enum
  • teams.ts - Safe JSON.stringify for circular refs + AbortController timeout on fetch (CodeRabbit findings)

Screenshots:

Provider picker showing Microsoft Teams as available:
teams-provider-picker

Create form:
teams-create-form

Edit form:
teams-edit-form

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.

Add Microsoft Teams as notification provider

2 participants