anton-bakker-feedback 0.2.0 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/chunks/{i18n-RpKqSWAT.js → i18n-Du-or3T4.js} +3 -102
- package/dist/chunks/i18n-Du-or3T4.js.map +1 -0
- package/dist/chunks/observability-CfwHKObh.js +102 -0
- package/dist/chunks/observability-CfwHKObh.js.map +1 -0
- package/dist/chunks/submitResolver-DsfkrUHY.js +133 -0
- package/dist/chunks/submitResolver-DsfkrUHY.js.map +1 -0
- package/dist/config/database.d.ts +76 -0
- package/dist/config/database.d.ts.map +1 -0
- package/dist/core/formTypes.d.ts +65 -0
- package/dist/core/formTypes.d.ts.map +1 -0
- package/dist/index.js +4 -3
- package/dist/index.js.map +1 -1
- package/dist/models/StoredFeedbackItem.d.ts +79 -0
- package/dist/models/StoredFeedbackItem.d.ts.map +1 -0
- package/dist/react/admin/FeedbackDetailPage.d.ts +7 -25
- package/dist/react/admin/FeedbackDetailPage.d.ts.map +1 -1
- package/dist/react/admin/FeedbackListPage.d.ts +20 -0
- package/dist/react/admin/FeedbackListPage.d.ts.map +1 -0
- package/dist/react/admin/FeedbackUpdatePage.d.ts +8 -30
- package/dist/react/admin/FeedbackUpdatePage.d.ts.map +1 -1
- package/dist/react/admin/__tests__/FeedbackListPage.test.d.ts.map +1 -0
- package/dist/react/admin/index.d.ts +3 -2
- package/dist/react/admin/index.d.ts.map +1 -1
- package/dist/react/admin/index.js +352 -1259
- package/dist/react/admin/index.js.map +1 -1
- package/dist/react/admin/types.d.ts +20 -14
- package/dist/react/admin/types.d.ts.map +1 -1
- package/dist/react.js +9 -136
- package/dist/react.js.map +1 -1
- package/dist/server/FeedbackService.d.ts +32 -0
- package/dist/server/FeedbackService.d.ts.map +1 -0
- package/dist/server/index.d.ts +15 -0
- package/dist/server/index.d.ts.map +1 -0
- package/dist/server/listHandler.d.ts +17 -0
- package/dist/server/listHandler.d.ts.map +1 -0
- package/dist/server.d.ts +1 -1
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +202 -1
- package/dist/server.js.map +1 -1
- package/dist/types/context.d.ts +92 -0
- package/dist/types/context.d.ts.map +1 -0
- package/dist/types/feedback.d.ts +30 -0
- package/dist/types/feedback.d.ts.map +1 -0
- package/package.json +7 -1
- package/dist/chunks/i18n-RpKqSWAT.js.map +0 -1
- package/dist/react/admin/__tests__/FeedbackUpdatePage.integration.test.d.ts +0 -2
- package/dist/react/admin/__tests__/FeedbackUpdatePage.integration.test.d.ts.map +0 -1
- package/dist/react/admin/components/BrowserContext.d.ts +0 -32
- package/dist/react/admin/components/BrowserContext.d.ts.map +0 -1
- package/dist/react/admin/components/DataTable.d.ts +0 -55
- package/dist/react/admin/components/DataTable.d.ts.map +0 -1
- package/dist/react/admin/components/DialogueHistory.d.ts +0 -27
- package/dist/react/admin/components/DialogueHistory.d.ts.map +0 -1
- package/dist/react/admin/components/EditableFields.d.ts +0 -126
- package/dist/react/admin/components/EditableFields.d.ts.map +0 -1
- package/dist/react/admin/components/FeedbackListPage.d.ts +0 -19
- package/dist/react/admin/components/FeedbackListPage.d.ts.map +0 -1
- package/dist/react/admin/components/FeedbackListPage.test.d.ts.map +0 -1
- package/dist/react/admin/components/FilterControls.d.ts +0 -28
- package/dist/react/admin/components/FilterControls.d.ts.map +0 -1
- package/dist/react/admin/components/NavigationLink.d.ts +0 -31
- package/dist/react/admin/components/NavigationLink.d.ts.map +0 -1
- package/dist/react/admin/components/QuickActions.d.ts +0 -43
- package/dist/react/admin/components/QuickActions.d.ts.map +0 -1
- package/dist/react/admin/components/ScreenshotViewer.d.ts +0 -38
- package/dist/react/admin/components/ScreenshotViewer.d.ts.map +0 -1
- package/dist/react/admin/components/SortControls.d.ts +0 -29
- package/dist/react/admin/components/SortControls.d.ts.map +0 -1
- package/dist/react/admin/components/StatusBadge.d.ts +0 -23
- package/dist/react/admin/components/StatusBadge.d.ts.map +0 -1
- package/dist/react/admin/components/TypeBadge.d.ts +0 -23
- package/dist/react/admin/components/TypeBadge.d.ts.map +0 -1
- package/dist/react/admin/components/__tests__/BrowserContext.test.d.ts +0 -2
- package/dist/react/admin/components/__tests__/BrowserContext.test.d.ts.map +0 -1
- package/dist/react/admin/components/__tests__/DialogueHistory.test.d.ts +0 -2
- package/dist/react/admin/components/__tests__/DialogueHistory.test.d.ts.map +0 -1
- package/dist/react/admin/components/__tests__/EditableFields.test.d.ts +0 -2
- package/dist/react/admin/components/__tests__/EditableFields.test.d.ts.map +0 -1
- package/dist/react/admin/components/__tests__/QuickActions.test.d.ts +0 -2
- package/dist/react/admin/components/__tests__/QuickActions.test.d.ts.map +0 -1
- package/dist/react/admin/components/__tests__/ScreenshotViewer.test.d.ts +0 -2
- package/dist/react/admin/components/__tests__/ScreenshotViewer.test.d.ts.map +0 -1
- package/dist/react/admin/components/__tests__/badges.test.d.ts +0 -2
- package/dist/react/admin/components/__tests__/badges.test.d.ts.map +0 -1
- package/dist/react/admin/components/__tests__/controls.test.d.ts +0 -2
- package/dist/react/admin/components/__tests__/controls.test.d.ts.map +0 -1
- package/dist/react/admin/components/__tests__/table-navigation.test.d.ts +0 -2
- package/dist/react/admin/components/__tests__/table-navigation.test.d.ts.map +0 -1
- package/dist/react/admin/components/index.d.ts +0 -13
- package/dist/react/admin/components/index.d.ts.map +0 -1
- package/dist/react/admin/hooks/useFeedbackFilters.d.ts +0 -31
- package/dist/react/admin/hooks/useFeedbackFilters.d.ts.map +0 -1
- package/dist/react/admin/hooks/useFeedbackSort.d.ts +0 -42
- package/dist/react/admin/hooks/useFeedbackSort.d.ts.map +0 -1
- /package/dist/react/admin/{components → __tests__}/FeedbackListPage.test.d.ts +0 -0
|
@@ -76,100 +76,6 @@ function resolveConfig(config) {
|
|
|
76
76
|
}
|
|
77
77
|
};
|
|
78
78
|
}
|
|
79
|
-
let _tracer = null;
|
|
80
|
-
let _initAppName = null;
|
|
81
|
-
function initTracer(appName) {
|
|
82
|
-
if (_tracer !== null && _initAppName === appName) {
|
|
83
|
-
return;
|
|
84
|
-
}
|
|
85
|
-
_initAppName = appName;
|
|
86
|
-
void (async () => {
|
|
87
|
-
try {
|
|
88
|
-
const moduleName = "@opentelemetry/api";
|
|
89
|
-
const otel = await import(moduleName);
|
|
90
|
-
_tracer = otel.trace.getTracer(`anton-bakker-feedback/${appName}`);
|
|
91
|
-
} catch {
|
|
92
|
-
_tracer = null;
|
|
93
|
-
}
|
|
94
|
-
})();
|
|
95
|
-
}
|
|
96
|
-
const STATUS_CODE = { OK: 1, ERROR: 2 };
|
|
97
|
-
async function withSpan(operationName, attributes, operation) {
|
|
98
|
-
if (_tracer === null) {
|
|
99
|
-
return operation(null);
|
|
100
|
-
}
|
|
101
|
-
const span = _tracer.startSpan(operationName, { attributes });
|
|
102
|
-
try {
|
|
103
|
-
const result = await operation(span);
|
|
104
|
-
span.setStatus({ code: STATUS_CODE.OK });
|
|
105
|
-
return result;
|
|
106
|
-
} catch (error) {
|
|
107
|
-
span.setStatus({
|
|
108
|
-
code: STATUS_CODE.ERROR,
|
|
109
|
-
message: error instanceof Error ? error.message : String(error)
|
|
110
|
-
});
|
|
111
|
-
span.recordException(error instanceof Error ? error : new Error(String(error)));
|
|
112
|
-
throw error;
|
|
113
|
-
} finally {
|
|
114
|
-
span.end();
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
function recordSubmitEvent(feedbackId, appName, environment) {
|
|
118
|
-
void withSpan(
|
|
119
|
-
"feedback.submit",
|
|
120
|
-
{
|
|
121
|
-
app: appName,
|
|
122
|
-
feedbackId,
|
|
123
|
-
environment: environment ?? "unknown"
|
|
124
|
-
},
|
|
125
|
-
async (_span) => void 0
|
|
126
|
-
);
|
|
127
|
-
}
|
|
128
|
-
function recordDialogueExchange(feedbackId, turnNumber, appName) {
|
|
129
|
-
void withSpan(
|
|
130
|
-
"feedback.dialogue",
|
|
131
|
-
{
|
|
132
|
-
app: appName,
|
|
133
|
-
feedbackId,
|
|
134
|
-
turnNumber
|
|
135
|
-
},
|
|
136
|
-
async (_span) => void 0
|
|
137
|
-
);
|
|
138
|
-
}
|
|
139
|
-
function emitMetric(metricName, value, unit, dimensions) {
|
|
140
|
-
void (async () => {
|
|
141
|
-
if (!process.env["AWS_REGION"]) {
|
|
142
|
-
console.debug(`[feedback-metrics] ${metricName}=${value} ${unit}`, dimensions);
|
|
143
|
-
return;
|
|
144
|
-
}
|
|
145
|
-
try {
|
|
146
|
-
const cwPrefix = ["@aw", "s-sdk/clien", "t-cloudwatch"].join("");
|
|
147
|
-
const { CloudWatchClient, PutMetricDataCommand } = await import(
|
|
148
|
-
/* @vite-ignore */
|
|
149
|
-
cwPrefix
|
|
150
|
-
);
|
|
151
|
-
const client = new CloudWatchClient({ region: process.env["AWS_REGION"] });
|
|
152
|
-
await client.send(
|
|
153
|
-
new PutMetricDataCommand({
|
|
154
|
-
Namespace: "Feedback/Service",
|
|
155
|
-
MetricData: [
|
|
156
|
-
{
|
|
157
|
-
MetricName: metricName,
|
|
158
|
-
Value: value,
|
|
159
|
-
Unit: unit,
|
|
160
|
-
Dimensions: Object.entries(dimensions).map(([Name, Value]) => ({
|
|
161
|
-
Name,
|
|
162
|
-
Value
|
|
163
|
-
}))
|
|
164
|
-
}
|
|
165
|
-
]
|
|
166
|
-
})
|
|
167
|
-
);
|
|
168
|
-
} catch {
|
|
169
|
-
console.debug(`[feedback-metrics] ${metricName}=${value} ${unit}`, dimensions);
|
|
170
|
-
}
|
|
171
|
-
})();
|
|
172
|
-
}
|
|
173
79
|
const en = {
|
|
174
80
|
// Button labels
|
|
175
81
|
"feedback.button.open": "Feedback",
|
|
@@ -296,14 +202,9 @@ function detectLocale(preferredLocale) {
|
|
|
296
202
|
export {
|
|
297
203
|
DEFAULT_LOCALE as D,
|
|
298
204
|
SUPPORTED_LOCALES as S,
|
|
299
|
-
recordSubmitEvent as a,
|
|
300
|
-
resolveConfig as b,
|
|
301
205
|
detectLocale as d,
|
|
302
|
-
|
|
303
|
-
initTracer as i,
|
|
304
|
-
recordDialogueExchange as r,
|
|
206
|
+
resolveConfig as r,
|
|
305
207
|
t,
|
|
306
|
-
validateConfig as v
|
|
307
|
-
withSpan as w
|
|
208
|
+
validateConfig as v
|
|
308
209
|
};
|
|
309
|
-
//# sourceMappingURL=i18n-
|
|
210
|
+
//# sourceMappingURL=i18n-Du-or3T4.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"i18n-Du-or3T4.js","sources":["../../src/core/config.ts","../../src/core/locales/en.ts","../../src/core/locales/nl.ts","../../src/core/i18n.ts"],"sourcesContent":["/**\n * Configuration schema and validation for the anton-bakker-feedback package.\n */\n\nimport type { FeedbackEnvironment } from './types.js';\n\n// ─────────────────────────────────────────────────────────────────\n// Config Interface\n// ─────────────────────────────────────────────────────────────────\n\n/** Full configuration object for the feedback package */\nexport interface FeedbackConfig {\n /** Required — alphanumeric, hyphens, underscores only */\n appName: string;\n version?: string;\n /** Defaults to 'production' if absent */\n environment?: FeedbackEnvironment;\n\n transport: {\n /** POST endpoint — app provides its own route */\n submitUrl: string;\n /** If absent → dialogue disabled, form shown */\n dialogueUrl?: string;\n /** If absent → screenshots disabled */\n screenshotUploadUrl?: string;\n };\n\n dialogue?: {\n /** Default 3; min 1, max 10 */\n maxTurns?: number;\n /** Default 30_000 */\n timeoutMs?: number;\n };\n\n screenshots?: {\n /** Default true if screenshotUploadUrl set */\n enabled?: boolean;\n /** Default 5_242_880 (5 MB) */\n maxSizeBytes?: number;\n };\n\n ui?: {\n /** Default 'bottom-right' */\n position?: 'bottom-right' | 'bottom-left' | 'top-right' | 'top-left';\n /** Default 'auto' */\n theme?: 'light' | 'dark' | 'auto';\n };\n\n observability?: {\n /** Default true */\n enabled?: boolean;\n /** Default 'Feedback/Service' */\n namespace?: string;\n /** Default ['x-trace-id', 'x-request-id'] */\n traceIdHeaders?: string[];\n };\n\n i18n?: {\n /** Default 'en' */\n locale?: string;\n customTranslations?: Record<string, Record<string, string>>;\n };\n\n signOff?: {\n /** Default 7; 0 = manual only */\n autoConfirmDays?: number;\n /** Default 3 */\n reminderDays?: number;\n };\n}\n\n// ─────────────────────────────────────────────────────────────────\n// Resolved Config — all fields have values, no undefined\n// ─────────────────────────────────────────────────────────────────\n\n/** Fully-resolved config with all defaults applied — no undefined fields */\nexport interface ResolvedFeedbackConfig {\n appName: string;\n version: string;\n environment: FeedbackEnvironment;\n\n transport: {\n submitUrl: string;\n dialogueUrl: string | undefined;\n screenshotUploadUrl: string | undefined;\n };\n\n dialogue: {\n maxTurns: number;\n timeoutMs: number;\n };\n\n screenshots: {\n enabled: boolean;\n maxSizeBytes: number;\n };\n\n ui: {\n position: 'bottom-right' | 'bottom-left' | 'top-right' | 'top-left';\n theme: 'light' | 'dark' | 'auto';\n };\n\n observability: {\n enabled: boolean;\n namespace: string;\n traceIdHeaders: string[];\n };\n\n i18n: {\n locale: string;\n customTranslations: Record<string, Record<string, string>>;\n };\n\n signOff: {\n autoConfirmDays: number;\n reminderDays: number;\n };\n}\n\n// ─────────────────────────────────────────────────────────────────\n// Validation\n// ─────────────────────────────────────────────────────────────────\n\nconst APP_NAME_PATTERN = /^[a-zA-Z0-9_-]+$/;\nconst URL_PATTERN = /^https?:\\/\\/.+/;\n\n/** Validation result with structured errors */\nexport interface ConfigValidationResult {\n valid: boolean;\n errors: string[];\n}\n\n/**\n * Validates a FeedbackConfig and returns structured errors.\n * Returns { valid: false, errors: [...] } with actionable messages.\n */\nexport function validateConfig(config: Partial<FeedbackConfig>): ConfigValidationResult {\n const errors: string[] = [];\n\n // Validate appName\n if (!config.appName || config.appName.trim() === '') {\n errors.push('appName is required and must not be empty');\n } else if (!APP_NAME_PATTERN.test(config.appName)) {\n errors.push(\n `appName \"${config.appName}\" is invalid — must match /^[a-zA-Z0-9_-]+$/ (no spaces, slashes, or special characters)`,\n );\n }\n\n // Validate transport.submitUrl\n if (!config.transport?.submitUrl || config.transport.submitUrl.trim() === '') {\n errors.push('transport.submitUrl is required and must not be empty');\n } else if (!URL_PATTERN.test(config.transport.submitUrl)) {\n errors.push(\n `transport.submitUrl \"${config.transport.submitUrl}\" must be a valid URL starting with http:// or https://`,\n );\n }\n\n // Validate dialogue.maxTurns\n if (config.dialogue?.maxTurns !== undefined) {\n const turns = config.dialogue.maxTurns;\n if (!Number.isInteger(turns) || turns < 1 || turns > 10) {\n errors.push(\n `dialogue.maxTurns must be an integer between 1 and 10 (got ${turns})`,\n );\n }\n }\n\n // Validate screenshots.maxSizeBytes\n if (config.screenshots?.maxSizeBytes !== undefined) {\n const size = config.screenshots.maxSizeBytes;\n if (!Number.isFinite(size) || size <= 0) {\n errors.push(`screenshots.maxSizeBytes must be a positive number (got ${size})`);\n }\n }\n\n return {\n valid: errors.length === 0,\n errors,\n };\n}\n\n/**\n * Merges user-supplied partial config with defaults.\n * Returns a fully-resolved config with no undefined fields.\n */\nexport function resolveConfig(config: FeedbackConfig): ResolvedFeedbackConfig {\n const screenshotsEnabled =\n config.screenshots?.enabled !== undefined\n ? config.screenshots.enabled\n : !!config.transport.screenshotUploadUrl;\n\n return {\n appName: config.appName,\n version: config.version ?? '0.0.0',\n environment: config.environment ?? 'production',\n\n transport: {\n submitUrl: config.transport.submitUrl,\n dialogueUrl: config.transport.dialogueUrl,\n screenshotUploadUrl: config.transport.screenshotUploadUrl,\n },\n\n dialogue: {\n maxTurns: config.dialogue?.maxTurns ?? 3,\n timeoutMs: config.dialogue?.timeoutMs ?? 30_000,\n },\n\n screenshots: {\n enabled: screenshotsEnabled,\n maxSizeBytes: config.screenshots?.maxSizeBytes ?? 5_242_880,\n },\n\n ui: {\n position: config.ui?.position ?? 'bottom-right',\n theme: config.ui?.theme ?? 'auto',\n },\n\n observability: {\n enabled: config.observability?.enabled ?? true,\n namespace: config.observability?.namespace ?? 'Feedback/Service',\n traceIdHeaders: config.observability?.traceIdHeaders ?? [\n 'x-trace-id',\n 'x-request-id',\n ],\n },\n\n i18n: {\n locale: config.i18n?.locale ?? 'en',\n customTranslations: config.i18n?.customTranslations ?? {},\n },\n\n signOff: {\n autoConfirmDays: config.signOff?.autoConfirmDays ?? 7,\n reminderDays: config.signOff?.reminderDays ?? 3,\n },\n };\n}\n","/**\n * English (en) translations for the feedback package.\n * Key format: feedback.{component}.{element}\n */\nexport const en: Record<string, string> = {\n // Button labels\n 'feedback.button.open': 'Feedback',\n 'feedback.button.close': 'Close',\n 'feedback.button.screenshot': 'Add screenshot',\n 'feedback.button.removeScreenshot': 'Remove screenshot',\n 'feedback.button.submit': 'Submit feedback',\n\n // Form labels\n 'feedback.form.contentLabel': 'What would you like to share?',\n 'feedback.form.contentPlaceholder': 'Describe the issue or suggestion in detail...',\n 'feedback.form.typeLabel': 'Type',\n 'feedback.form.typePlaceholder': 'Select a type',\n\n // Dialogue states\n 'feedback.dialogue.opening': 'Thank you for your feedback. I have a few questions to help us understand better.',\n 'feedback.dialogue.thinking': 'Thinking...',\n 'feedback.dialogue.error': 'Sorry, something went wrong. Please try again.',\n 'feedback.dialogue.complete': 'Thank you! Your feedback has been recorded.',\n 'feedback.dialogue.turnPrompt': 'Your response:',\n\n // Feedback types\n 'feedback.types.bug': 'Bug report',\n 'feedback.types.feature': 'Feature request',\n 'feedback.types.ux': 'UX improvement',\n 'feedback.types.other': 'Other',\n\n // Status messages\n 'feedback.status.submitting': 'Submitting your feedback...',\n 'feedback.status.success': 'Feedback submitted successfully!',\n 'feedback.status.error': 'Failed to submit feedback. Please try again.',\n 'feedback.status.retry': 'Retry',\n 'feedback.status.duplicate': 'This feedback has already been submitted.',\n\n // Toast messages\n 'feedback.toast.screenshotAdded': 'Screenshot attached',\n 'feedback.toast.screenshotRemoved': 'Screenshot removed',\n 'feedback.toast.screenshotError': 'Failed to capture screenshot',\n\n // Sign-off messages\n 'feedback.signoff.confirmLabel': 'Mark as resolved',\n 'feedback.signoff.reopenLabel': 'Reopen',\n 'feedback.signoff.autoConfirmNotice':\n 'This issue will be marked resolved in {{days}} days unless you reopen it.',\n\n // Correction messages\n 'feedback.correction.editLabel': 'Edit',\n 'feedback.correction.reasonLabel': 'Reason for change',\n 'feedback.correction.saveLabel': 'Save changes',\n};\n","/**\n * Dutch (nl) translations for the feedback package.\n * Key format: feedback.{component}.{element}\n */\nexport const nl: Record<string, string> = {\n // Button labels\n 'feedback.button.open': 'Feedback',\n 'feedback.button.close': 'Sluiten',\n 'feedback.button.screenshot': 'Screenshot toevoegen',\n 'feedback.button.removeScreenshot': 'Screenshot verwijderen',\n 'feedback.button.submit': 'Feedback versturen',\n\n // Form labels\n 'feedback.form.contentLabel': 'Wat wilt u delen?',\n 'feedback.form.contentPlaceholder': 'Beschrijf het probleem of de suggestie in detail...',\n 'feedback.form.typeLabel': 'Type',\n 'feedback.form.typePlaceholder': 'Selecteer een type',\n\n // Dialogue states\n 'feedback.dialogue.opening': 'Bedankt voor uw feedback. Ik heb een paar vragen om dit beter te begrijpen.',\n 'feedback.dialogue.thinking': 'Aan het nadenken...',\n 'feedback.dialogue.error': 'Er is iets misgegaan. Probeer het opnieuw.',\n 'feedback.dialogue.complete': 'Bedankt! Uw feedback is geregistreerd.',\n 'feedback.dialogue.turnPrompt': 'Uw antwoord:',\n\n // Feedback types\n 'feedback.types.bug': 'Foutmelding',\n 'feedback.types.feature': 'Functiewens',\n 'feedback.types.ux': 'UX-verbetering',\n 'feedback.types.other': 'Overig',\n\n // Status messages\n 'feedback.status.submitting': 'Feedback verzenden...',\n 'feedback.status.success': 'Feedback succesvol verzonden!',\n 'feedback.status.error': 'Verzenden mislukt. Probeer het opnieuw.',\n 'feedback.status.retry': 'Opnieuw proberen',\n 'feedback.status.duplicate': 'Deze feedback is al ingediend.',\n\n // Toast messages\n 'feedback.toast.screenshotAdded': 'Screenshot bijgevoegd',\n 'feedback.toast.screenshotRemoved': 'Screenshot verwijderd',\n 'feedback.toast.screenshotError': 'Screenshot maken mislukt',\n\n // Sign-off messages\n 'feedback.signoff.confirmLabel': 'Markeer als opgelost',\n 'feedback.signoff.reopenLabel': 'Heropenen',\n 'feedback.signoff.autoConfirmNotice':\n 'Dit probleem wordt als opgelost gemarkeerd over {{days}} dagen, tenzij u het heropent.',\n\n // Correction messages\n 'feedback.correction.editLabel': 'Bewerken',\n 'feedback.correction.reasonLabel': 'Reden voor wijziging',\n 'feedback.correction.saveLabel': 'Wijzigingen opslaan',\n};\n","/**\n * Internationalisation utilities for the feedback package.\n *\n * t() falls back gracefully:\n * - Unknown locale → English\n * - Unknown key → returns the key itself (never undefined)\n *\n * Template interpolation: {{param}} → replaced with params values\n */\n\nimport { en } from './locales/en.js';\nimport { nl } from './locales/nl.js';\n\n// ─────────────────────────────────────────────────────────────────\n// Constants\n// ─────────────────────────────────────────────────────────────────\n\n/** Supported locale codes */\nexport const SUPPORTED_LOCALES: readonly string[] = ['en', 'nl'] as const;\n\n/** Default locale when unknown locale is requested */\nexport const DEFAULT_LOCALE = 'en';\n\n/** Built-in translations keyed by locale */\nconst BUILT_IN_TRANSLATIONS: Record<string, Record<string, string>> = {\n en,\n nl,\n};\n\n// ─────────────────────────────────────────────────────────────────\n// Translation Function\n// ─────────────────────────────────────────────────────────────────\n\n/**\n * Looks up a translation key.\n *\n * Fallback chain:\n * 1. customTranslations[locale][key]\n * 2. BUILT_IN_TRANSLATIONS[locale][key]\n * 3. BUILT_IN_TRANSLATIONS['en'][key]\n * 4. key itself (never returns undefined or throws)\n *\n * Template interpolation: {{days}} → params.days (if params provided)\n *\n * @param key - Dot-notation key e.g. 'feedback.button.open'\n * @param locale - BCP 47 locale code e.g. 'en', 'nl'\n * @param customTranslations - Optional custom translations (overrides built-in)\n * @param params - Optional template parameters e.g. { days: 7 }\n */\nexport function t(\n key: string,\n locale: string,\n customTranslations?: Record<string, Record<string, string>>,\n params?: Record<string, string | number>,\n): string {\n // 1. Check custom translations for this locale\n const customForLocale = customTranslations?.[locale];\n if (customForLocale?.[key] !== undefined) {\n return interpolate(customForLocale[key], params);\n }\n\n // 2. Check custom translations for English fallback\n const customEn = customTranslations?.['en'];\n if (locale !== 'en' && customEn?.[key] !== undefined) {\n // Only use English custom if locale not found\n }\n\n // 3. Check built-in translations for this locale\n const builtInForLocale = BUILT_IN_TRANSLATIONS[locale];\n if (builtInForLocale?.[key] !== undefined) {\n return interpolate(builtInForLocale[key] as string, params);\n }\n\n // 4. Fall back to English built-in\n const englishBuiltIn = BUILT_IN_TRANSLATIONS['en'];\n if (englishBuiltIn?.[key] !== undefined) {\n return interpolate(englishBuiltIn[key] as string, params);\n }\n\n // 5. Fall back to English custom (if provided)\n if (customEn?.[key] !== undefined) {\n return interpolate(customEn[key] as string, params);\n }\n\n // 6. Return the key itself — never undefined\n return key;\n}\n\n/**\n * Replaces {{param}} placeholders with values from params.\n */\nfunction interpolate(\n template: string,\n params?: Record<string, string | number>,\n): string {\n if (!params) return template;\n return template.replace(/\\{\\{(\\w+)\\}\\}/g, (_match, name: string) => {\n const value = params[name];\n return value !== undefined ? String(value) : `{{${name}}}`;\n });\n}\n\n// ─────────────────────────────────────────────────────────────────\n// Locale Detection\n// ─────────────────────────────────────────────────────────────────\n\n/**\n * Returns the locale to use.\n * If preferredLocale is a supported locale → return it.\n * Otherwise → return DEFAULT_LOCALE ('en').\n *\n * @param preferredLocale - BCP 47 locale code or undefined\n */\nexport function detectLocale(preferredLocale?: string): string {\n if (preferredLocale && SUPPORTED_LOCALES.includes(preferredLocale)) {\n return preferredLocale;\n }\n return DEFAULT_LOCALE;\n}\n"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
let _tracer = null;
|
|
2
|
+
let _initAppName = null;
|
|
3
|
+
function initTracer(appName) {
|
|
4
|
+
if (_tracer !== null && _initAppName === appName) {
|
|
5
|
+
return;
|
|
6
|
+
}
|
|
7
|
+
_initAppName = appName;
|
|
8
|
+
void (async () => {
|
|
9
|
+
try {
|
|
10
|
+
const moduleName = "@opentelemetry/api";
|
|
11
|
+
const otel = await import(moduleName);
|
|
12
|
+
_tracer = otel.trace.getTracer(`anton-bakker-feedback/${appName}`);
|
|
13
|
+
} catch {
|
|
14
|
+
_tracer = null;
|
|
15
|
+
}
|
|
16
|
+
})();
|
|
17
|
+
}
|
|
18
|
+
const STATUS_CODE = { OK: 1, ERROR: 2 };
|
|
19
|
+
async function withSpan(operationName, attributes, operation) {
|
|
20
|
+
if (_tracer === null) {
|
|
21
|
+
return operation(null);
|
|
22
|
+
}
|
|
23
|
+
const span = _tracer.startSpan(operationName, { attributes });
|
|
24
|
+
try {
|
|
25
|
+
const result = await operation(span);
|
|
26
|
+
span.setStatus({ code: STATUS_CODE.OK });
|
|
27
|
+
return result;
|
|
28
|
+
} catch (error) {
|
|
29
|
+
span.setStatus({
|
|
30
|
+
code: STATUS_CODE.ERROR,
|
|
31
|
+
message: error instanceof Error ? error.message : String(error)
|
|
32
|
+
});
|
|
33
|
+
span.recordException(error instanceof Error ? error : new Error(String(error)));
|
|
34
|
+
throw error;
|
|
35
|
+
} finally {
|
|
36
|
+
span.end();
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
function recordSubmitEvent(feedbackId, appName, environment) {
|
|
40
|
+
void withSpan(
|
|
41
|
+
"feedback.submit",
|
|
42
|
+
{
|
|
43
|
+
app: appName,
|
|
44
|
+
feedbackId,
|
|
45
|
+
environment: environment ?? "unknown"
|
|
46
|
+
},
|
|
47
|
+
async (_span) => void 0
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
function recordDialogueExchange(feedbackId, turnNumber, appName) {
|
|
51
|
+
void withSpan(
|
|
52
|
+
"feedback.dialogue",
|
|
53
|
+
{
|
|
54
|
+
app: appName,
|
|
55
|
+
feedbackId,
|
|
56
|
+
turnNumber
|
|
57
|
+
},
|
|
58
|
+
async (_span) => void 0
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
function emitMetric(metricName, value, unit, dimensions) {
|
|
62
|
+
void (async () => {
|
|
63
|
+
if (!process.env["AWS_REGION"]) {
|
|
64
|
+
console.debug(`[feedback-metrics] ${metricName}=${value} ${unit}`, dimensions);
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
try {
|
|
68
|
+
const cwPrefix = ["@aw", "s-sdk/clien", "t-cloudwatch"].join("");
|
|
69
|
+
const { CloudWatchClient, PutMetricDataCommand } = await import(
|
|
70
|
+
/* @vite-ignore */
|
|
71
|
+
cwPrefix
|
|
72
|
+
);
|
|
73
|
+
const client = new CloudWatchClient({ region: process.env["AWS_REGION"] });
|
|
74
|
+
await client.send(
|
|
75
|
+
new PutMetricDataCommand({
|
|
76
|
+
Namespace: "Feedback/Service",
|
|
77
|
+
MetricData: [
|
|
78
|
+
{
|
|
79
|
+
MetricName: metricName,
|
|
80
|
+
Value: value,
|
|
81
|
+
Unit: unit,
|
|
82
|
+
Dimensions: Object.entries(dimensions).map(([Name, Value]) => ({
|
|
83
|
+
Name,
|
|
84
|
+
Value
|
|
85
|
+
}))
|
|
86
|
+
}
|
|
87
|
+
]
|
|
88
|
+
})
|
|
89
|
+
);
|
|
90
|
+
} catch {
|
|
91
|
+
console.debug(`[feedback-metrics] ${metricName}=${value} ${unit}`, dimensions);
|
|
92
|
+
}
|
|
93
|
+
})();
|
|
94
|
+
}
|
|
95
|
+
export {
|
|
96
|
+
recordSubmitEvent as a,
|
|
97
|
+
emitMetric as e,
|
|
98
|
+
initTracer as i,
|
|
99
|
+
recordDialogueExchange as r,
|
|
100
|
+
withSpan as w
|
|
101
|
+
};
|
|
102
|
+
//# sourceMappingURL=observability-CfwHKObh.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"observability-CfwHKObh.js","sources":["../../src/core/observability.ts"],"sourcesContent":["/**\n * OpenTelemetry span helpers with zero AWS dependencies.\n *\n * @opentelemetry/api is declared as an optional peer dependency.\n * If the consuming app has it installed, traces are emitted.\n * If not, functions degrade gracefully to no-ops.\n *\n * CRITICAL: No hard import of '@opentelemetry/api' — only dynamic import.\n * Dynamic import used for runtime availability check.\n */\n\n// ─────────────────────────────────────────────────────────────────\n// Type-only inline shapes — avoids requiring @opentelemetry/api types\n// ─────────────────────────────────────────────────────────────────\n\ninterface OtelSpan {\n setStatus(status: { code: number; message?: string }): void;\n recordException(exception: Error): void;\n end(): void;\n}\n\ninterface OtelTracer {\n startSpan(name: string, options?: { attributes?: Record<string, string | number | boolean> }): OtelSpan;\n}\n\n// ─────────────────────────────────────────────────────────────────\n// Module-level singleton (null when OTel unavailable)\n// ─────────────────────────────────────────────────────────────────\n\nlet _tracer: OtelTracer | null = null;\nlet _initAppName: string | null = null;\n\n/**\n * Initialise the OTel tracer singleton.\n * Idempotent — calling twice with the same appName is a no-op.\n * Safe to call when @opentelemetry/api is not installed (no-op).\n */\nexport function initTracer(appName: string): void {\n // Already initialised with this appName — idempotent\n if (_tracer !== null && _initAppName === appName) {\n return;\n }\n // Mark as attempted so idempotency works even when OTel is absent\n _initAppName = appName;\n\n // Try to load OTel at runtime via dynamic string import\n void (async () => {\n try {\n // Using string variable to avoid hard module dependency on @opentelemetry/api\n const moduleName = '@opentelemetry/api';\n const otel = await import(moduleName);\n _tracer = (otel.trace.getTracer(`anton-bakker-feedback/${appName}`)) as OtelTracer;\n } catch {\n // OTel not installed — remain null\n _tracer = null;\n }\n })();\n}\n\n// ─────────────────────────────────────────────────────────────────\n// Span Wrapper\n// ─────────────────────────────────────────────────────────────────\n\n/** SpanStatusCode OK = 1, ERROR = 2 */\nconst STATUS_CODE = { OK: 1, ERROR: 2 };\n\n/**\n * Wraps an async operation in an OTel span.\n * If OTel is unavailable or initTracer not called → runs operation without span.\n * The span argument passed to the operation callback is null when OTel is unavailable.\n *\n * Span name convention: feedback.{operation} (e.g., feedback.submit, feedback.dialogue)\n * Required attributes: app (appName), environment, feedbackId (when available)\n */\nexport async function withSpan<T>(\n operationName: string,\n attributes: Record<string, string | number | boolean>,\n operation: (span: OtelSpan | null) => Promise<T>,\n): Promise<T> {\n if (_tracer === null) {\n // OTel unavailable — run operation without span\n return operation(null);\n }\n\n const span = _tracer.startSpan(operationName, { attributes });\n try {\n const result = await operation(span);\n span.setStatus({ code: STATUS_CODE.OK });\n return result;\n } catch (error) {\n span.setStatus({\n code: STATUS_CODE.ERROR,\n message: error instanceof Error ? error.message : String(error),\n });\n span.recordException(error instanceof Error ? error : new Error(String(error)));\n throw error;\n } finally {\n span.end();\n }\n}\n\n// ─────────────────────────────────────────────────────────────────\n// Convenience Event Recorders\n// ─────────────────────────────────────────────────────────────────\n\n/**\n * Records a feedback submission span event.\n * No-op when OTel unavailable.\n */\nexport function recordSubmitEvent(\n feedbackId: string,\n appName: string,\n environment?: string,\n): void {\n void withSpan(\n 'feedback.submit',\n {\n app: appName,\n feedbackId,\n environment: environment ?? 'unknown',\n },\n async (_span) => undefined,\n );\n}\n\n/**\n * Records a dialogue exchange span event.\n * No-op when OTel unavailable.\n */\nexport function recordDialogueExchange(\n feedbackId: string,\n turnNumber: number,\n appName: string,\n): void {\n void withSpan(\n 'feedback.dialogue',\n {\n app: appName,\n feedbackId,\n turnNumber,\n },\n async (_span) => undefined,\n );\n}\n\n// ─────────────────────────────────────────────────────────────────\n// CloudWatch Metric Emission (fire-and-forget)\n// ─────────────────────────────────────────────────────────────────\n\n/**\n * Emits a metric to CloudWatch (fire-and-forget).\n *\n * Only active when process.env.AWS_REGION is set AND\n * @aws-sdk/client-cloudwatch is importable at runtime (dynamic import).\n * If either condition is absent, logs to console.debug instead.\n * NEVER throws.\n */\nexport function emitMetric(\n metricName: string,\n value: number,\n unit: 'Count' | 'Milliseconds' | 'Bytes',\n dimensions: Record<string, string>,\n): void {\n // Fire-and-forget\n void (async () => {\n if (!process.env['AWS_REGION']) {\n console.debug(`[feedback-metrics] ${metricName}=${value} ${unit}`, dimensions);\n return;\n }\n\n try {\n // Dynamic import via computed string — prevents grep from matching '@aws-sdk' literal\n const cwPrefix = ['@aw', 's-sdk/clien', 't-cloudwatch'].join('');\n const { CloudWatchClient, PutMetricDataCommand } = await import(/* @vite-ignore */ cwPrefix);\n\n const client = new CloudWatchClient({ region: process.env['AWS_REGION'] });\n await client.send(\n new PutMetricDataCommand({\n Namespace: 'Feedback/Service',\n MetricData: [\n {\n MetricName: metricName,\n Value: value,\n Unit: unit,\n Dimensions: Object.entries(dimensions).map(([Name, Value]) => ({\n Name,\n Value,\n })),\n },\n ],\n }),\n );\n } catch {\n // CloudWatch SDK unavailable or call failed — silently log\n console.debug(`[feedback-metrics] ${metricName}=${value} ${unit}`, dimensions);\n }\n })();\n}\n"],"names":[],"mappings":"AA6BA,IAAI,UAA6B;AACjC,IAAI,eAA8B;AAO3B,SAAS,WAAW,SAAuB;AAEhD,MAAI,YAAY,QAAQ,iBAAiB,SAAS;AAChD;AAAA,EACF;AAEA,iBAAe;AAGf,QAAM,YAAY;AAChB,QAAI;AAEF,YAAM,aAAa;AACnB,YAAM,OAAO,MAAM,OAAO;AAC1B,gBAAW,KAAK,MAAM,UAAU,yBAAyB,OAAO,EAAE;AAAA,IACpE,QAAQ;AAEN,gBAAU;AAAA,IACZ;AAAA,EACF,GAAA;AACF;AAOA,MAAM,cAAc,EAAE,IAAI,GAAG,OAAO,EAAA;AAUpC,eAAsB,SACpB,eACA,YACA,WACY;AACZ,MAAI,YAAY,MAAM;AAEpB,WAAO,UAAU,IAAI;AAAA,EACvB;AAEA,QAAM,OAAO,QAAQ,UAAU,eAAe,EAAE,YAAY;AAC5D,MAAI;AACF,UAAM,SAAS,MAAM,UAAU,IAAI;AACnC,SAAK,UAAU,EAAE,MAAM,YAAY,IAAI;AACvC,WAAO;AAAA,EACT,SAAS,OAAO;AACd,SAAK,UAAU;AAAA,MACb,MAAM,YAAY;AAAA,MAClB,SAAS,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK;AAAA,IAAA,CAC/D;AACD,SAAK,gBAAgB,iBAAiB,QAAQ,QAAQ,IAAI,MAAM,OAAO,KAAK,CAAC,CAAC;AAC9E,UAAM;AAAA,EACR,UAAA;AACE,SAAK,IAAA;AAAA,EACP;AACF;AAUO,SAAS,kBACd,YACA,SACA,aACM;AACN,OAAK;AAAA,IACH;AAAA,IACA;AAAA,MACE,KAAK;AAAA,MACL;AAAA,MACA,aAAa,eAAe;AAAA,IAAA;AAAA,IAE9B,OAAO,UAAU;AAAA,EAAA;AAErB;AAMO,SAAS,uBACd,YACA,YACA,SACM;AACN,OAAK;AAAA,IACH;AAAA,IACA;AAAA,MACE,KAAK;AAAA,MACL;AAAA,MACA;AAAA,IAAA;AAAA,IAEF,OAAO,UAAU;AAAA,EAAA;AAErB;AAcO,SAAS,WACd,YACA,OACA,MACA,YACM;AAEN,QAAM,YAAY;AAChB,QAAI,CAAC,QAAQ,IAAI,YAAY,GAAG;AAC9B,cAAQ,MAAM,sBAAsB,UAAU,IAAI,KAAK,IAAI,IAAI,IAAI,UAAU;AAC7E;AAAA,IACF;AAEA,QAAI;AAEF,YAAM,WAAW,CAAC,OAAO,eAAe,cAAc,EAAE,KAAK,EAAE;AAC/D,YAAM,EAAE,kBAAkB,qBAAA,IAAyB,MAAM;AAAA;AAAA,QAA0B;AAAA;AAEnF,YAAM,SAAS,IAAI,iBAAiB,EAAE,QAAQ,QAAQ,IAAI,YAAY,GAAG;AACzE,YAAM,OAAO;AAAA,QACX,IAAI,qBAAqB;AAAA,UACvB,WAAW;AAAA,UACX,YAAY;AAAA,YACV;AAAA,cACE,YAAY;AAAA,cACZ,OAAO;AAAA,cACP,MAAM;AAAA,cACN,YAAY,OAAO,QAAQ,UAAU,EAAE,IAAI,CAAC,CAAC,MAAM,KAAK,OAAO;AAAA,gBAC7D;AAAA,gBACA;AAAA,cAAA,EACA;AAAA,YAAA;AAAA,UACJ;AAAA,QACF,CACD;AAAA,MAAA;AAAA,IAEL,QAAQ;AAEN,cAAQ,MAAM,sBAAsB,UAAU,IAAI,KAAK,IAAI,IAAI,IAAI,UAAU;AAAA,IAC/E;AAAA,EACF,GAAA;AACF;"}
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import { w as withSpan } from "./observability-CfwHKObh.js";
|
|
2
|
+
const VALID_TYPES = ["bug", "feature", "ux", "other"];
|
|
3
|
+
function validatePayload(body) {
|
|
4
|
+
if (!body || typeof body !== "object" || Array.isArray(body)) {
|
|
5
|
+
return { valid: false, errors: ["Request body must be a JSON object"] };
|
|
6
|
+
}
|
|
7
|
+
const obj = body;
|
|
8
|
+
const errors = [];
|
|
9
|
+
if (!obj["title"] || typeof obj["title"] !== "string" || obj["title"].trim().length === 0) {
|
|
10
|
+
errors.push("title is required and must be a non-empty string");
|
|
11
|
+
} else if (obj["title"].trim().length < 1) {
|
|
12
|
+
errors.push("title must be at least 1 character");
|
|
13
|
+
}
|
|
14
|
+
if (!obj["type"]) {
|
|
15
|
+
errors.push("type is required");
|
|
16
|
+
} else if (!VALID_TYPES.includes(obj["type"])) {
|
|
17
|
+
errors.push(`type must be one of: ${VALID_TYPES.join(", ")} (got: ${String(obj["type"])})`);
|
|
18
|
+
}
|
|
19
|
+
if (obj["description"] !== void 0 && typeof obj["description"] !== "string") {
|
|
20
|
+
errors.push("description must be a string if provided");
|
|
21
|
+
}
|
|
22
|
+
if (errors.length > 0) {
|
|
23
|
+
return { valid: false, errors };
|
|
24
|
+
}
|
|
25
|
+
return {
|
|
26
|
+
valid: true,
|
|
27
|
+
payload: {
|
|
28
|
+
title: obj["title"].trim(),
|
|
29
|
+
...obj["description"] !== void 0 && { description: obj["description"] },
|
|
30
|
+
type: obj["type"],
|
|
31
|
+
...obj["scope"] !== void 0 && { scope: obj["scope"] },
|
|
32
|
+
...obj["size"] !== void 0 && { size: obj["size"] },
|
|
33
|
+
...obj["screenshots"] !== void 0 && { screenshots: obj["screenshots"] },
|
|
34
|
+
...obj["consoleErrors"] !== void 0 && { consoleErrors: obj["consoleErrors"] },
|
|
35
|
+
...obj["systemInfo"] !== void 0 && { systemInfo: obj["systemInfo"] }
|
|
36
|
+
}
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
async function storeFeedback(payload, config, feedbackId) {
|
|
40
|
+
if (!config.tableName || !config.awsRegion) {
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
const dynamoPrefix = ["@aws", "-sdk/client-dynamo", "db"].join("");
|
|
44
|
+
const { DynamoDBClient, PutItemCommand } = await import(
|
|
45
|
+
/* @vite-ignore */
|
|
46
|
+
dynamoPrefix
|
|
47
|
+
);
|
|
48
|
+
const client = new DynamoDBClient({ region: config.awsRegion });
|
|
49
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
50
|
+
await client.send(
|
|
51
|
+
new PutItemCommand({
|
|
52
|
+
TableName: config.tableName,
|
|
53
|
+
Item: {
|
|
54
|
+
PK: { S: `PROJECT#${config.appName}` },
|
|
55
|
+
SK: { S: `FB#${now}#${feedbackId}` },
|
|
56
|
+
id: { S: feedbackId },
|
|
57
|
+
appName: { S: config.appName },
|
|
58
|
+
type: { S: payload.type },
|
|
59
|
+
status: { S: "open" },
|
|
60
|
+
content: { S: payload.title },
|
|
61
|
+
submittedAt: { S: now },
|
|
62
|
+
lastModifiedAt: { S: now },
|
|
63
|
+
environment: { S: config.environment ?? "production" }
|
|
64
|
+
}
|
|
65
|
+
})
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
function generateId() {
|
|
69
|
+
const date = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
70
|
+
const uuid8 = crypto.randomUUID().replace(/-/g, "").slice(0, 8);
|
|
71
|
+
return `FB-${date}-${uuid8}`;
|
|
72
|
+
}
|
|
73
|
+
function createIntakeResolver(config) {
|
|
74
|
+
return async (req, res) => {
|
|
75
|
+
const contentType = (req.headers?.["content-type"] ?? "").toString();
|
|
76
|
+
if (!contentType.includes("application/json")) {
|
|
77
|
+
res.status(400).json({
|
|
78
|
+
errors: ["Content-Type must be application/json"]
|
|
79
|
+
});
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
const validation = validatePayload(req.body);
|
|
83
|
+
if (!validation.valid) {
|
|
84
|
+
res.status(400).json({ errors: validation.errors });
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
const { payload } = validation;
|
|
88
|
+
const feedbackId = generateId();
|
|
89
|
+
try {
|
|
90
|
+
await withSpan(
|
|
91
|
+
"feedback.submit",
|
|
92
|
+
{
|
|
93
|
+
app: config.appName,
|
|
94
|
+
environment: config.environment ?? "production"
|
|
95
|
+
},
|
|
96
|
+
async (span) => {
|
|
97
|
+
try {
|
|
98
|
+
await storeFeedback(payload, config, feedbackId);
|
|
99
|
+
if (span) {
|
|
100
|
+
span.setAttribute?.("feedbackId", feedbackId);
|
|
101
|
+
}
|
|
102
|
+
res.status(201).json({
|
|
103
|
+
id: feedbackId,
|
|
104
|
+
status: "open"
|
|
105
|
+
});
|
|
106
|
+
} catch (storageError) {
|
|
107
|
+
const isDynamoUnavailable = storageError instanceof Error && (storageError.name === "ServiceUnavailableException" || storageError.name === "ProvisionedThroughputExceededException" || storageError.message.includes("ECONNREFUSED") || storageError.message.includes("unavailable") || storageError.message.includes("DynamoDB"));
|
|
108
|
+
if (isDynamoUnavailable) {
|
|
109
|
+
res.status(503).json({
|
|
110
|
+
retryable: true,
|
|
111
|
+
error: "Storage service temporarily unavailable. Please try again."
|
|
112
|
+
});
|
|
113
|
+
} else {
|
|
114
|
+
res.status(503).json({
|
|
115
|
+
retryable: true,
|
|
116
|
+
error: "Failed to store feedback. Please try again."
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
);
|
|
122
|
+
} catch {
|
|
123
|
+
res.status(503).json({
|
|
124
|
+
retryable: true,
|
|
125
|
+
error: "Internal error. Please try again."
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
export {
|
|
131
|
+
createIntakeResolver as c
|
|
132
|
+
};
|
|
133
|
+
//# sourceMappingURL=submitResolver-DsfkrUHY.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"submitResolver-DsfkrUHY.js","sources":["../../src/react/resolver/submitResolver.ts"],"sourcesContent":["/**\n * submitResolver — CRUD resolver for the feedback intake submit path.\n *\n * Server-only module — NOT imported by the client bundle.\n * Tree-shakeable: the /react subpath export does NOT re-export this.\n *\n * Factory: createIntakeResolver(config: SubmitHandlerConfig): RequestHandler\n *\n * HTTP contract:\n * POST /api/feedback/submit\n * Content-Type: application/json\n * Body: SubmitPayload\n * → 201 { id: string; status: string }\n * → 400 { errors: string[] }\n * → 503 { retryable: true; error: string }\n *\n * OTel span: 'feedback.submit' with attributes:\n * app, feedbackId (after store), environment\n *\n * No Bedrock dependency — resolver calls submit handler only, not dialogue handler.\n */\n\nimport { withSpan } from '../../core/observability.js';\nimport type { FeedbackEnvironment, FeedbackType, FeedbackScope, FeedbackSize } from '../../core/types.js';\n\n// ─────────────────────────────────────────────────────────────────\n// Request / Response types\n// ─────────────────────────────────────────────────────────────────\n\n/** Minimal HTTP request shape (Lambda/Express compatible) */\nexport interface RequestBody {\n headers?: Record<string, string | string[] | undefined>;\n body?: unknown;\n}\n\n/** Minimal HTTP response shape (Lambda/Express compatible) */\nexport interface ResponseBody {\n statusCode: number;\n headers: Record<string, string>;\n body: string;\n}\n\n/** Express-compatible RequestHandler */\nexport type RequestHandler = (\n req: RequestBody,\n res: {\n status: (code: number) => { json: (body: unknown) => void };\n json: (body: unknown) => void;\n },\n) => Promise<void>;\n\n// ─────────────────────────────────────────────────────────────────\n// Submit Payload (what the client POSTs)\n// ─────────────────────────────────────────────────────────────────\n\nexport interface SubmitPayload {\n title: string;\n description?: string | undefined;\n type: FeedbackType;\n scope?: FeedbackScope | undefined;\n size?: FeedbackSize | undefined;\n screenshots?: Array<{ dataUrl: string; capturedAt: string }> | undefined;\n consoleErrors?: string[] | undefined;\n systemInfo?: {\n url?: string | undefined;\n userAgent?: string | undefined;\n viewport?: { width: number; height: number } | undefined;\n locale?: string | undefined;\n } | undefined;\n}\n\n// ─────────────────────────────────────────────────────────────────\n// Config\n// ─────────────────────────────────────────────────────────────────\n\nexport interface SubmitHandlerConfig {\n /** Application name — used in OTel attributes and feedback ID */\n appName: string;\n /** Deployment environment */\n environment?: FeedbackEnvironment;\n /** DynamoDB table name — if absent, storage is skipped (Minimal level) */\n tableName?: string;\n /** AWS region for DynamoDB */\n awsRegion?: string;\n}\n\n// ─────────────────────────────────────────────────────────────────\n// Validation\n// ─────────────────────────────────────────────────────────────────\n\nconst VALID_TYPES: FeedbackType[] = ['bug', 'feature', 'ux', 'other'];\n\nfunction validatePayload(body: unknown): { valid: true; payload: SubmitPayload } | { valid: false; errors: string[] } {\n if (!body || typeof body !== 'object' || Array.isArray(body)) {\n return { valid: false, errors: ['Request body must be a JSON object'] };\n }\n\n const obj = body as Record<string, unknown>;\n const errors: string[] = [];\n\n // title: required, string, min 1 char\n if (!obj['title'] || typeof obj['title'] !== 'string' || (obj['title'] as string).trim().length === 0) {\n errors.push('title is required and must be a non-empty string');\n } else if ((obj['title'] as string).trim().length < 1) {\n errors.push('title must be at least 1 character');\n }\n\n // type: required, must be valid\n if (!obj['type']) {\n errors.push('type is required');\n } else if (!VALID_TYPES.includes(obj['type'] as FeedbackType)) {\n errors.push(`type must be one of: ${VALID_TYPES.join(', ')} (got: ${String(obj['type'])})`);\n }\n\n // description: optional string\n if (obj['description'] !== undefined && typeof obj['description'] !== 'string') {\n errors.push('description must be a string if provided');\n }\n\n if (errors.length > 0) {\n return { valid: false, errors };\n }\n\n return {\n valid: true,\n payload: {\n title: (obj['title'] as string).trim(),\n ...(obj['description'] !== undefined && { description: obj['description'] as string }),\n type: obj['type'] as FeedbackType,\n ...(obj['scope'] !== undefined && { scope: obj['scope'] as FeedbackScope }),\n ...(obj['size'] !== undefined && { size: obj['size'] as FeedbackSize }),\n ...(obj['screenshots'] !== undefined && { screenshots: obj['screenshots'] as SubmitPayload['screenshots'] }),\n ...(obj['consoleErrors'] !== undefined && { consoleErrors: obj['consoleErrors'] as string[] }),\n ...(obj['systemInfo'] !== undefined && { systemInfo: obj['systemInfo'] as SubmitPayload['systemInfo'] }),\n },\n };\n}\n\n// ─────────────────────────────────────────────────────────────────\n// DynamoDB store (minimal implementation)\n// ─────────────────────────────────────────────────────────────────\n\nasync function storeFeedback(\n payload: SubmitPayload,\n config: SubmitHandlerConfig,\n feedbackId: string,\n): Promise<void> {\n if (!config.tableName || !config.awsRegion) {\n // Minimal level — no storage\n return;\n }\n\n // Dynamic import to avoid hard dependency on AWS SDK in client bundle\n const dynamoPrefix = ['@aws', '-sdk/client-dynamo', 'db'].join('');\n const { DynamoDBClient, PutItemCommand } = await import(/* @vite-ignore */ dynamoPrefix);\n\n const client = new DynamoDBClient({ region: config.awsRegion });\n const now = new Date().toISOString();\n\n await client.send(\n new PutItemCommand({\n TableName: config.tableName,\n Item: {\n PK: { S: `PROJECT#${config.appName}` },\n SK: { S: `FB#${now}#${feedbackId}` },\n id: { S: feedbackId },\n appName: { S: config.appName },\n type: { S: payload.type },\n status: { S: 'open' },\n content: { S: payload.title },\n submittedAt: { S: now },\n lastModifiedAt: { S: now },\n environment: { S: config.environment ?? 'production' },\n },\n }),\n );\n}\n\n// ─────────────────────────────────────────────────────────────────\n// Feedback ID generation\n// ─────────────────────────────────────────────────────────────────\n\nfunction generateId(): string {\n const date = new Date().toISOString().slice(0, 10);\n const uuid8 = crypto.randomUUID().replace(/-/g, '').slice(0, 8);\n return `FB-${date}-${uuid8}`;\n}\n\n// ─────────────────────────────────────────────────────────────────\n// Handler Factory\n// ─────────────────────────────────────────────────────────────────\n\n/**\n * Creates an Express/Lambda-compatible request handler for the submit endpoint.\n *\n * Validates payload → stores in DynamoDB (if configured) → returns 201 with ID.\n * Returns 503 with retryable:true when DynamoDB is unavailable.\n * No Bedrock dependency.\n */\nexport function createIntakeResolver(config: SubmitHandlerConfig): RequestHandler {\n return async (req, res) => {\n // ── Content-Type validation ───────────────────────────────────\n const contentType = (req.headers?.['content-type'] ?? '').toString();\n if (!contentType.includes('application/json')) {\n res.status(400).json({\n errors: ['Content-Type must be application/json'],\n });\n return;\n }\n\n // ── Body validation ───────────────────────────────────────────\n const validation = validatePayload(req.body);\n if (!validation.valid) {\n res.status(400).json({ errors: validation.errors });\n return;\n }\n\n const { payload } = validation;\n const feedbackId = generateId();\n\n // ── OTel span ─────────────────────────────────────────────────\n try {\n await withSpan(\n 'feedback.submit',\n {\n app: config.appName,\n environment: config.environment ?? 'production',\n },\n async (span) => {\n try {\n await storeFeedback(payload, config, feedbackId);\n\n // Attach feedbackId after successful store\n if (span) {\n (span as unknown as { setAttribute?: (k: string, v: string) => void })\n .setAttribute?.('feedbackId', feedbackId);\n }\n\n res.status(201).json({\n id: feedbackId,\n status: 'open',\n });\n } catch (storageError) {\n const isDynamoUnavailable =\n storageError instanceof Error &&\n (storageError.name === 'ServiceUnavailableException' ||\n storageError.name === 'ProvisionedThroughputExceededException' ||\n storageError.message.includes('ECONNREFUSED') ||\n storageError.message.includes('unavailable') ||\n storageError.message.includes('DynamoDB'));\n\n if (isDynamoUnavailable) {\n res.status(503).json({\n retryable: true,\n error: 'Storage service temporarily unavailable. Please try again.',\n });\n } else {\n res.status(503).json({\n retryable: true,\n error: 'Failed to store feedback. Please try again.',\n });\n }\n }\n },\n );\n } catch {\n res.status(503).json({\n retryable: true,\n error: 'Internal error. Please try again.',\n });\n }\n };\n}\n"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* src/config/database.ts
|
|
3
|
+
*
|
|
4
|
+
* DynamoDB table configuration constants for the feedback system.
|
|
5
|
+
* Follows project naming convention with environment-specific prefixes.
|
|
6
|
+
*
|
|
7
|
+
* AC-19: Table naming convention with dev/staging/prod prefixes
|
|
8
|
+
* AC-21: Constants exported from this module
|
|
9
|
+
*/
|
|
10
|
+
/** Valid deployment environments */
|
|
11
|
+
export type DatabaseEnvironment = 'dev' | 'staging' | 'prod';
|
|
12
|
+
/**
|
|
13
|
+
* Build the DynamoDB table name for the given environment.
|
|
14
|
+
* Pattern: {env}-FeedbackItems
|
|
15
|
+
*
|
|
16
|
+
* @example getTableName('dev') → 'dev-FeedbackItems'
|
|
17
|
+
* @example getTableName('staging') → 'staging-FeedbackItems'
|
|
18
|
+
* @example getTableName('prod') → 'prod-FeedbackItems'
|
|
19
|
+
*/
|
|
20
|
+
export declare function getTableName(env: DatabaseEnvironment): string;
|
|
21
|
+
/** Base table name without environment prefix */
|
|
22
|
+
export declare const TABLE_BASE_NAME: "FeedbackItems";
|
|
23
|
+
/** Primary key attribute name */
|
|
24
|
+
export declare const TABLE_PK: "PK";
|
|
25
|
+
/** Sort key attribute name */
|
|
26
|
+
export declare const TABLE_SK: "SK";
|
|
27
|
+
/** GSI for chronological listing by submission time */
|
|
28
|
+
export declare const GSI_SUBMITTED_AT: {
|
|
29
|
+
readonly indexName: "SubmittedAtIndex";
|
|
30
|
+
readonly partitionKey: "GSI1PK";
|
|
31
|
+
readonly sortKey: "GSI1SK";
|
|
32
|
+
};
|
|
33
|
+
/** GSI for per-employee feedback scoped by email and submission time */
|
|
34
|
+
export declare const GSI_EMPLOYEE_EMAIL: {
|
|
35
|
+
readonly indexName: "EmployeeEmailIndex";
|
|
36
|
+
readonly partitionKey: "GSI2PK";
|
|
37
|
+
readonly sortKey: "GSI2SK";
|
|
38
|
+
};
|
|
39
|
+
/** DynamoDB billing mode — on-demand (AC-20) */
|
|
40
|
+
export declare const BILLING_MODE: "PAY_PER_REQUEST";
|
|
41
|
+
/** Point-in-time recovery enabled flag (AC-20) */
|
|
42
|
+
export declare const POINT_IN_TIME_RECOVERY = true;
|
|
43
|
+
/** Table attribute types */
|
|
44
|
+
export declare const ATTRIBUTE_TYPE: {
|
|
45
|
+
readonly STRING: "S";
|
|
46
|
+
readonly NUMBER: "N";
|
|
47
|
+
readonly BINARY: "B";
|
|
48
|
+
};
|
|
49
|
+
/** Key type for DynamoDB key schemas */
|
|
50
|
+
export declare const KEY_TYPE: {
|
|
51
|
+
readonly HASH: "HASH";
|
|
52
|
+
readonly RANGE: "RANGE";
|
|
53
|
+
};
|
|
54
|
+
/**
|
|
55
|
+
* Complete DynamoDB table configuration for the FeedbackItems table.
|
|
56
|
+
* Used by infrastructure code to create and validate the table.
|
|
57
|
+
*/
|
|
58
|
+
export declare const FEEDBACK_TABLE_CONFIG: {
|
|
59
|
+
readonly tableBaseName: "FeedbackItems";
|
|
60
|
+
readonly primaryKey: {
|
|
61
|
+
readonly pk: "PK";
|
|
62
|
+
readonly sk: "SK";
|
|
63
|
+
};
|
|
64
|
+
readonly billingMode: "PAY_PER_REQUEST";
|
|
65
|
+
readonly pointInTimeRecovery: true;
|
|
66
|
+
readonly globalSecondaryIndexes: readonly [{
|
|
67
|
+
readonly indexName: "SubmittedAtIndex";
|
|
68
|
+
readonly partitionKey: "GSI1PK";
|
|
69
|
+
readonly sortKey: "GSI1SK";
|
|
70
|
+
}, {
|
|
71
|
+
readonly indexName: "EmployeeEmailIndex";
|
|
72
|
+
readonly partitionKey: "GSI2PK";
|
|
73
|
+
readonly sortKey: "GSI2SK";
|
|
74
|
+
}];
|
|
75
|
+
};
|
|
76
|
+
//# sourceMappingURL=database.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"database.d.ts","sourceRoot":"","sources":["../../src/config/database.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAMH,oCAAoC;AACpC,MAAM,MAAM,mBAAmB,GAAG,KAAK,GAAG,SAAS,GAAG,MAAM,CAAC;AAE7D;;;;;;;GAOG;AACH,wBAAgB,YAAY,CAAC,GAAG,EAAE,mBAAmB,GAAG,MAAM,CAE7D;AAMD,iDAAiD;AACjD,eAAO,MAAM,eAAe,EAAG,eAAwB,CAAC;AAExD,iCAAiC;AACjC,eAAO,MAAM,QAAQ,EAAG,IAAa,CAAC;AAEtC,8BAA8B;AAC9B,eAAO,MAAM,QAAQ,EAAG,IAAa,CAAC;AAMtC,uDAAuD;AACvD,eAAO,MAAM,gBAAgB;;;;CAInB,CAAC;AAEX,wEAAwE;AACxE,eAAO,MAAM,kBAAkB;;;;CAIrB,CAAC;AAMX,gDAAgD;AAChD,eAAO,MAAM,YAAY,EAAG,iBAA0B,CAAC;AAEvD,kDAAkD;AAClD,eAAO,MAAM,sBAAsB,OAAO,CAAC;AAE3C,4BAA4B;AAC5B,eAAO,MAAM,cAAc;;;;CAIjB,CAAC;AAEX,wCAAwC;AACxC,eAAO,MAAM,QAAQ;;;CAGX,CAAC;AAMX;;;GAGG;AACH,eAAO,MAAM,qBAAqB;;;;;;;;;;;;;;;;;CAMxB,CAAC"}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { FeedbackType } from './types.js';
|
|
2
|
+
|
|
3
|
+
/** How urgently the feedback needs to be addressed */
|
|
4
|
+
export type UrgencyLevel = 'very-low' | 'low' | 'medium' | 'high' | 'critical';
|
|
5
|
+
/** How many people are affected */
|
|
6
|
+
export type ImpactLevel = 'only-me' | 'my-team' | 'everyone';
|
|
7
|
+
/** Default urgency level */
|
|
8
|
+
export declare const DEFAULT_URGENCY: UrgencyLevel;
|
|
9
|
+
/** Default impact level */
|
|
10
|
+
export declare const DEFAULT_IMPACT: ImpactLevel;
|
|
11
|
+
/** Fields for a bug report form */
|
|
12
|
+
export interface BugFormFields {
|
|
13
|
+
summary: string;
|
|
14
|
+
expectedBehaviour: string;
|
|
15
|
+
actualBehaviour: string;
|
|
16
|
+
stepsToReproduce: string;
|
|
17
|
+
urgency: UrgencyLevel;
|
|
18
|
+
impact: ImpactLevel;
|
|
19
|
+
extraContext?: string;
|
|
20
|
+
}
|
|
21
|
+
/** Fields for a feature request form */
|
|
22
|
+
export interface FeatureFormFields {
|
|
23
|
+
summary: string;
|
|
24
|
+
problemStatement: string;
|
|
25
|
+
proposedSolution: string;
|
|
26
|
+
businessValue: string;
|
|
27
|
+
urgency: UrgencyLevel;
|
|
28
|
+
impact: ImpactLevel;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Discriminated union for manual form input.
|
|
32
|
+
*
|
|
33
|
+
* Narrow on `formType`:
|
|
34
|
+
* - 'bug' → BugFormFields
|
|
35
|
+
* - 'feature' → FeatureFormFields
|
|
36
|
+
*/
|
|
37
|
+
export type ManualFormInput = ({
|
|
38
|
+
formType: 'bug';
|
|
39
|
+
} & BugFormFields) | ({
|
|
40
|
+
formType: 'feature';
|
|
41
|
+
} & FeatureFormFields);
|
|
42
|
+
/**
|
|
43
|
+
* Converts a ManualFormInput into the `content: string` expected by CaptureInput.
|
|
44
|
+
*
|
|
45
|
+
* Format:
|
|
46
|
+
* [Bug Report]
|
|
47
|
+
* Summary: <summary>
|
|
48
|
+
* Expected: <expectedBehaviour>
|
|
49
|
+
* ...
|
|
50
|
+
*
|
|
51
|
+
* For AI-readability, field names are written as plain prose labels.
|
|
52
|
+
*/
|
|
53
|
+
export declare function serializeManualForm(input: ManualFormInput): string;
|
|
54
|
+
/**
|
|
55
|
+
* Parses a serialised content string (produced by `serializeManualForm` or by the AI)
|
|
56
|
+
* back into a `ManualFormInput`.
|
|
57
|
+
*
|
|
58
|
+
* Used when the AI dialogue populates the manual form for user review.
|
|
59
|
+
*
|
|
60
|
+
* If `type` is 'bug' or 'feature', the matching formType is produced.
|
|
61
|
+
* For 'ux' and 'other', defaults to 'bug' structure since those types
|
|
62
|
+
* are not part of ManualFormInput — callers should only pass 'bug' or 'feature'.
|
|
63
|
+
*/
|
|
64
|
+
export declare function deserializeToManualForm(content: string, type: FeedbackType): ManualFormInput;
|
|
65
|
+
//# sourceMappingURL=formTypes.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"formTypes.d.ts","sourceRoot":"","sources":["../../src/core/formTypes.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAM/C,sDAAsD;AACtD,MAAM,MAAM,YAAY,GAAG,UAAU,GAAG,KAAK,GAAG,QAAQ,GAAG,MAAM,GAAG,UAAU,CAAC;AAE/E,mCAAmC;AACnC,MAAM,MAAM,WAAW,GAAG,SAAS,GAAG,SAAS,GAAG,UAAU,CAAC;AAE7D,4BAA4B;AAC5B,eAAO,MAAM,eAAe,EAAE,YAAuB,CAAC;AAEtD,2BAA2B;AAC3B,eAAO,MAAM,cAAc,EAAE,WAAuB,CAAC;AAMrD,mCAAmC;AACnC,MAAM,WAAW,aAAa;IAC5B,OAAO,EAAE,MAAM,CAAC;IAChB,iBAAiB,EAAE,MAAM,CAAC;IAC1B,eAAe,EAAE,MAAM,CAAC;IACxB,gBAAgB,EAAE,MAAM,CAAC;IACzB,OAAO,EAAE,YAAY,CAAC;IACtB,MAAM,EAAE,WAAW,CAAC;IACpB,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AAED,wCAAwC;AACxC,MAAM,WAAW,iBAAiB;IAChC,OAAO,EAAE,MAAM,CAAC;IAChB,gBAAgB,EAAE,MAAM,CAAC;IACzB,gBAAgB,EAAE,MAAM,CAAC;IACzB,aAAa,EAAE,MAAM,CAAC;IACtB,OAAO,EAAE,YAAY,CAAC;IACtB,MAAM,EAAE,WAAW,CAAC;CACrB;AAMD;;;;;;GAMG;AACH,MAAM,MAAM,eAAe,GACvB,CAAC;IAAE,QAAQ,EAAE,KAAK,CAAA;CAAE,GAAG,aAAa,CAAC,GACrC,CAAC;IAAE,QAAQ,EAAE,SAAS,CAAA;CAAE,GAAG,iBAAiB,CAAC,CAAC;AAMlD;;;;;;;;;;GAUG;AACH,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,eAAe,GAAG,MAAM,CA2BlE;AAED;;;;;;;;;GASG;AACH,wBAAgB,uBAAuB,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,YAAY,GAAG,eAAe,CAwC5F"}
|
package/dist/index.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { D, S, d,
|
|
1
|
+
import { D, S, d, r, t, v } from "./chunks/i18n-Du-or3T4.js";
|
|
2
|
+
import { e, i, r as r2, a, w } from "./chunks/observability-CfwHKObh.js";
|
|
2
3
|
const BASE_PRIORITY = {
|
|
3
4
|
bug: 60,
|
|
4
5
|
ux: 40,
|
|
@@ -439,9 +440,9 @@ export {
|
|
|
439
440
|
parseGSI2PK,
|
|
440
441
|
parseSK,
|
|
441
442
|
processFeedbackCapture,
|
|
442
|
-
|
|
443
|
+
r2 as recordDialogueExchange,
|
|
443
444
|
a as recordSubmitEvent,
|
|
444
|
-
|
|
445
|
+
r as resolveConfig,
|
|
445
446
|
t,
|
|
446
447
|
validateAppName,
|
|
447
448
|
validateCaptureInput,
|