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
1 change: 1 addition & 0 deletions .obsidian/app.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{}
1 change: 1 addition & 0 deletions .obsidian/appearance.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{}
33 changes: 33 additions & 0 deletions .obsidian/core-plugins.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
{
"file-explorer": true,
"global-search": true,
"switcher": true,
"graph": true,
"backlink": true,
"canvas": true,
"outgoing-link": true,
"tag-pane": true,
"footnotes": false,
"properties": true,
"page-preview": true,
"daily-notes": true,
"templates": true,
"note-composer": true,
"command-palette": true,
"slash-command": false,
"editor-status": true,
"bookmarks": true,
"markdown-importer": false,
"zk-prefixer": false,
"random-note": false,
"outline": true,
"word-count": true,
"slides": false,
"audio-recorder": false,
"workspaces": false,
"file-recovery": true,
"publish": false,
"sync": true,
"bases": true,
"webviewer": false
}
216 changes: 216 additions & 0 deletions .obsidian/workspace.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,216 @@
{
"main": {
"id": "8c9386b19c91147b",
"type": "split",
"children": [
{
"id": "8864c640b792b187",
"type": "tabs",
"children": [
{
"id": "1429c13e9a05c89c",
"type": "leaf",
"state": {
"type": "markdown",
"state": {
"file": "product/iterations/README.md",
"mode": "source",
"source": false
},
"icon": "lucide-file",
"title": "README"
}
}
]
}
],
"direction": "vertical"
},
"left": {
"id": "679b90edf1780cc9",
"type": "split",
"children": [
{
"id": "3942d2d324c197b4",
"type": "tabs",
"children": [
{
"id": "34d31476315ecc4d",
"type": "leaf",
"state": {
"type": "file-explorer",
"state": {
"sortOrder": "alphabetical",
"autoReveal": false
},
"icon": "lucide-folder-closed",
"title": "Files"
}
},
{
"id": "12a7d42e402fcd44",
"type": "leaf",
"state": {
"type": "search",
"state": {
"query": "",
"matchingCase": false,
"explainSearch": false,
"collapseAll": false,
"extraContext": false,
"sortOrder": "alphabetical"
},
"icon": "lucide-search",
"title": "Search"
}
},
{
"id": "3989c6374832e5dd",
"type": "leaf",
"state": {
"type": "bookmarks",
"state": {},
"icon": "lucide-bookmark",
"title": "Bookmarks"
}
}
]
}
],
"direction": "horizontal",
"width": 300
},
"right": {
"id": "c3c8283d06725f20",
"type": "split",
"children": [
{
"id": "0aea389e684d0637",
"type": "tabs",
"children": [
{
"id": "48cb1f074dc3d555",
"type": "leaf",
"state": {
"type": "backlink",
"state": {
"file": "product/iterations/README.md",
"collapseAll": false,
"extraContext": false,
"sortOrder": "alphabetical",
"showSearch": false,
"searchQuery": "",
"backlinkCollapsed": false,
"unlinkedCollapsed": true
},
"icon": "links-coming-in",
"title": "Backlinks for README"
}
},
{
"id": "0dc7eff9844a1a31",
"type": "leaf",
"state": {
"type": "outgoing-link",
"state": {
"file": "product/iterations/README.md",
"linksCollapsed": false,
"unlinkedCollapsed": true
},
"icon": "links-going-out",
"title": "Outgoing links from README"
}
},
{
"id": "cc9802a54c87c0fd",
"type": "leaf",
"state": {
"type": "tag",
"state": {
"sortOrder": "frequency",
"useHierarchy": true,
"showSearch": false,
"searchQuery": ""
},
"icon": "lucide-tags",
"title": "Tags"
}
},
{
"id": "e726800e5550aaff",
"type": "leaf",
"state": {
"type": "all-properties",
"state": {
"sortOrder": "frequency",
"showSearch": false,
"searchQuery": ""
},
"icon": "lucide-archive",
"title": "All properties"
}
},
{
"id": "15f16edd4dcfd933",
"type": "leaf",
"state": {
"type": "outline",
"state": {
"file": "product/iterations/README.md",
"followCursor": false,
"showSearch": false,
"searchQuery": ""
},
"icon": "lucide-list",
"title": "Outline of README"
}
}
]
}
],
"direction": "horizontal",
"width": 300,
"collapsed": true
},
"left-ribbon": {
"hiddenItems": {
"switcher:Open quick switcher": false,
"graph:Open graph view": false,
"canvas:Create new canvas": false,
"daily-notes:Open today's daily note": false,
"templates:Insert template": false,
"command-palette:Open command palette": false,
"bases:Create new base": false
}
},
"active": "1429c13e9a05c89c",
"lastOpenFiles": [
"product/iterations/2025-12-06-webhooks/stories/story-056-slack-payload-formatting.md",
"product/iterations/2025-12-06-webhooks/stories/story-055-webhook-error-logging.md",
"product/iterations/2025-12-06-webhooks/stories/story-054-webhook-config-file.md",
"product/iterations/2025-12-06-webhooks/stories/story-053-summary-payload-schema.md",
"product/iterations/2025-12-06-webhooks/stories/story-052-webhook-dispatch-on-submission.md",
"product/iterations/2025-12-06-webhooks/stories/stories-index.md",
"product/iterations/2025-12-06-webhooks/stories/story-053-slack-channel-integration.md",
"product/iterations/2025-12-06-webhooks/stories/story-052-webhook-notification-service.md",
"product/iterations/2025-12-06-webhooks/discovery/synthesis/synthesis-2025-12-06.md",
"product/iterations/2025-12-06-webhooks/discovery/interviews/interview-jeremy-2025-12-06.md",
"product/iterations/2025-12-06-webhooks/discovery/interviews/interview-template.md",
"product/iterations/2025-12-06-webhooks/discovery/README.md",
"product/iterations/2025-12-06-webhooks/design",
"product/iterations/2025-12-06-webhooks/story-maps",
"product/iterations/2025-12-06-webhooks/stories",
"product/iterations/2025-12-06-webhooks/discovery/synthesis",
"product/iterations/2025-12-06-webhooks/discovery/observations",
"product/iterations/2025-12-06-webhooks/discovery/interviews",
"product/iterations/2025-12-06-webhooks/discovery",
"product/iterations/2025-12-06-webhooks",
"product/iterations/2025-12-06-sandbox-js1/discovery/interviews/interview-jeremy-2025-12-06.md",
"product/iterations/2025-12-06-sandbox-js1/discovery/interviews/interview-template.md",
"product/iterations/2025-12-06-sandbox-js1/discovery/README.md",
"product/iterations/2025-12-06-sandbox-js1/design",
"product/iterations/2025-12-06-sandbox-js1/story-maps",
"2025-11-12-mvp.md",
"product/iterations/README.md"
]
}
5 changes: 4 additions & 1 deletion apps/backend/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,7 @@ PORT=3001
NODE_ENV=development

# Frontend CORS
FRONTEND_URL=http://localhost:3000
FRONTEND_URL=http://localhost:3000

# Webhook Configuration (optional)
WEBHOOK_URL=https://webhook.site/6165643b-3526-4086-8ceb-523bc0633375
2 changes: 2 additions & 0 deletions apps/backend/src/modules/survey/survey.module.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import { Module } from '@nestjs/common';
import { SurveyController } from './survey.controller';
import { SurveyService } from './survey.service';
import { WebhookModule } from '../webhook/webhook.module';

@Module({
imports: [WebhookModule],
controllers: [SurveyController],
providers: [SurveyService],
exports: [SurveyService],
Expand Down
16 changes: 15 additions & 1 deletion apps/backend/src/modules/survey/survey.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,17 @@ import { PrismaService } from '../../prisma/prisma.service';
import { CreateSurveyResponseDto } from './dto/create-survey-response.dto';
import { SurveyResponseDto } from './dto/survey-response.dto';
import { Role, Status } from '@prisma/client';
import { WebhookService } from '../webhook/webhook.service';

@Injectable()
export class SurveyService {
private readonly logger = new Logger(SurveyService.name);
private readonly ANONYMOUS_EMAIL = 'anonymous@survey.local';

constructor(private readonly prisma: PrismaService) {}
constructor(
private readonly prisma: PrismaService,
private readonly webhookService: WebhookService,
) {}

/**
* Get or create the anonymous user for survey submissions
Expand Down Expand Up @@ -130,6 +134,16 @@ export class SurveyService {
// Log submission (without PII)
this.logger.log(`Survey submitted: ${result.id}`);

// Dispatch webhook asynchronously (don't block response)
this.webhookService
.handleSurveySubmission(result.id, dto.q1OverallRating ?? null)
.catch((error) => {
this.logger.error(
`Webhook dispatch failed for submission ${result.id}`,
error instanceof Error ? error.stack : String(error),
);
});

// Return response DTO
return new SurveyResponseDto(
result.id,
Expand Down
Loading