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
4 changes: 4 additions & 0 deletions src/lib/config.server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,10 @@ export const SLACK_USER_FEEDBACK_WEBHOOK_URL = getEnvVariable('SLACK_USER_FEEDBA
// Posts deploy threat alerts to a dedicated Slack channel.
// Expected to be a Slack Incoming Webhook URL.
export const SLACK_DEPLOY_THREAT_WEBHOOK_URL = getEnvVariable('SLACK_DEPLOY_THREAT_WEBHOOK_URL');
// Bot token + channel for internal Kilo feedback notifications via chat.postMessage.
// The bot token must have the chat:write scope.
export const SLACK_FEEDBACK_BOT_TOKEN = getEnvVariable('SLACK_FEEDBACK_BOT_TOKEN');
export const SLACK_FEEDBACK_CHANNEL_ID = getEnvVariable('SLACK_FEEDBACK_CHANNEL_ID');
export const ENABLE_MILVUS_DUAL_WRITE = true;

// AI Attribution Service
Expand Down
53 changes: 53 additions & 0 deletions src/lib/feedback/app-builder-feedback-slack.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import 'server-only';

import {
postInternalSlackMessage,
postInternalSlackThreadReply,
} from '@/lib/slack/internal-notifications';

const FEEDBACK_PREVIEW_LIMIT = 500;

/**
* Sends an app builder feedback notification to the internal Kilo Slack channel.
* If the feedback text exceeds the preview limit, the full text is posted as a thread reply.
* Fire-and-forget safe — catches and logs all errors.
*/
export async function notifyAppBuilderFeedback(
projectId: string,
feedbackText: string
): Promise<void> {
const adminLink = `https://app.kilo.ai/admin/app-builder/${projectId}`;
const trimmedFeedback = feedbackText.trim();
const wasTruncated = trimmedFeedback.length > FEEDBACK_PREVIEW_LIMIT;
const previewText =
trimmedFeedback.slice(0, FEEDBACK_PREVIEW_LIMIT) + (wasTruncated ? '...' : '');

try {
const ts = await postInternalSlackMessage(
[
{
type: 'header',
text: {
type: 'plain_text',
text: 'New App Builder feedback :hammer_and_wrench:',
},
},
{
type: 'section',
text: { type: 'mrkdwn', text: `<${adminLink}|View project>` },
},
{
type: 'section',
text: { type: 'plain_text', text: previewText },
},
],
'New App Builder feedback'
);

if (wasTruncated && ts) {
await postInternalSlackThreadReply(ts, trimmedFeedback);
}
} catch (error) {
console.error('[AppBuilderFeedback] Failed to post to Slack', error);
}
}
57 changes: 57 additions & 0 deletions src/lib/slack/internal-notifications.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import 'server-only';

import { WebClient } from '@slack/web-api';
import type { Block, KnownBlock } from '@slack/web-api';
import { SLACK_FEEDBACK_BOT_TOKEN, SLACK_FEEDBACK_CHANNEL_ID } from '@/lib/config.server';

function makeClient(): WebClient {
if (!SLACK_FEEDBACK_BOT_TOKEN) {
throw new Error('SLACK_FEEDBACK_BOT_TOKEN is not configured');
}
return new WebClient(SLACK_FEEDBACK_BOT_TOKEN);
}

/**
* Posts a message to the internal Kilo Slack feedback channel.
* Returns the message ts on success (needed for threading), or undefined if not configured.
* Throws on API errors.
*/
export async function postInternalSlackMessage(
blocks: (Block | KnownBlock)[],
fallbackText: string
): Promise<string | undefined> {
if (!SLACK_FEEDBACK_BOT_TOKEN || !SLACK_FEEDBACK_CHANNEL_ID) {
return undefined;
}

const client = makeClient();
const result = await client.chat.postMessage({
channel: SLACK_FEEDBACK_CHANNEL_ID,
text: fallbackText,
unfurl_links: false,
unfurl_media: false,
blocks,
});

return result.ts;
}

/**
* Posts a thread reply to an existing message in the internal Kilo Slack feedback channel.
* Throws on API errors.
*/
export async function postInternalSlackThreadReply(
threadTs: string,
text: string
): Promise<void> {
if (!SLACK_FEEDBACK_BOT_TOKEN || !SLACK_FEEDBACK_CHANNEL_ID) {
return;
}

const client = makeClient();
await client.chat.postMessage({
channel: SLACK_FEEDBACK_CHANNEL_ID,
thread_ts: threadTs,
text,
});
}
46 changes: 4 additions & 42 deletions src/routers/app-builder-feedback-router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { app_builder_feedback } from '@/db/schema';
import { getProjectWithOwnershipCheck } from '@/lib/app-builder/app-builder-service';
import { ensureOrganizationAccess } from '@/routers/organizations/utils';
import * as z from 'zod';
import { SLACK_USER_FEEDBACK_WEBHOOK_URL } from '@/lib/config.server';
import { notifyAppBuilderFeedback } from '@/lib/feedback/app-builder-feedback-slack';
import type { Owner } from '@/lib/integrations/core/types';

const recentMessageSchema = z.object({
Expand Down Expand Up @@ -57,47 +57,9 @@ export const appBuilderFeedbackRouter = createTRPCRouter({
.returning({ id: app_builder_feedback.id });

// Best-effort Slack notification
if (SLACK_USER_FEEDBACK_WEBHOOK_URL) {
const adminLink = `https://app.kilo.ai/admin/app-builder/${input.project_id}`;
const trimmedFeedback = input.feedback_text.trim();
const wasTruncated = trimmedFeedback.length > 500;
const feedbackText = trimmedFeedback.slice(0, 500) + (wasTruncated ? '...' : '');

fetch(SLACK_USER_FEEDBACK_WEBHOOK_URL, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
text: 'New App Builder feedback',
unfurl_links: false,
unfurl_media: false,
blocks: [
{
type: 'header',
text: {
type: 'plain_text',
text: 'New App Builder feedback :hammer_and_wrench:',
},
},
{
type: 'section',
text: {
type: 'mrkdwn',
text: `<${adminLink}|View project>`,
},
},
{
type: 'section',
text: {
type: 'plain_text',
text: feedbackText,
},
},
],
}),
}).catch(error => {
console.error('[AppBuilderFeedback] Failed to post to Slack webhook', error);
});
}
notifyAppBuilderFeedback(input.project_id, input.feedback_text).catch(error => {
console.error('[AppBuilderFeedback] Failed to post to Slack', error);
});
Copy link
Contributor Author

Choose a reason for hiding this comment

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

[SUGGESTION]: Dead .catch() handler — notifyAppBuilderFeedback already catches and logs all errors internally (see its try/catch on lines 25–52 of app-builder-feedback-slack.ts), so this promise will never reject.

You can simplify to a bare call:

Suggested change
});
notifyAppBuilderFeedback(input.project_id, input.feedback_text);

Alternatively, if you'd prefer the router to own error logging, remove the try/catch inside notifyAppBuilderFeedback and keep this .catch() instead.


return inserted;
}),
Expand Down
Loading