anton-bakker-feedback 0.1.0
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 +309 -0
- package/dist/chunks/i18n-RpKqSWAT.js.map +1 -0
- package/dist/core/capture.d.ts +57 -0
- package/dist/core/capture.d.ts.map +1 -0
- package/dist/core/config.d.ts +107 -0
- package/dist/core/config.d.ts.map +1 -0
- package/dist/core/errors.d.ts +80 -0
- package/dist/core/errors.d.ts.map +1 -0
- package/dist/core/i18n.d.ts +39 -0
- package/dist/core/i18n.d.ts.map +1 -0
- package/dist/core/index.d.ts +16 -0
- package/dist/core/index.d.ts.map +1 -0
- package/dist/core/locales/en.d.ts +6 -0
- package/dist/core/locales/en.d.ts.map +1 -0
- package/dist/core/locales/nl.d.ts +6 -0
- package/dist/core/locales/nl.d.ts.map +1 -0
- package/dist/core/observability.d.ts +54 -0
- package/dist/core/observability.d.ts.map +1 -0
- package/dist/core/transforms.d.ts +57 -0
- package/dist/core/transforms.d.ts.map +1 -0
- package/dist/core/transport.d.ts +44 -0
- package/dist/core/transport.d.ts.map +1 -0
- package/dist/core/types.d.ts +130 -0
- package/dist/core/types.d.ts.map +1 -0
- package/dist/core/validation.d.ts +48 -0
- package/dist/core/validation.d.ts.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +455 -0
- package/dist/index.js.map +1 -0
- package/dist/manifest.d.ts +2 -0
- package/dist/manifest.d.ts.map +1 -0
- package/dist/manifest.js +2 -0
- package/dist/manifest.js.map +1 -0
- package/dist/react/FeedbackButton.d.ts +19 -0
- package/dist/react/FeedbackButton.d.ts.map +1 -0
- package/dist/react/FeedbackConfirm.d.ts +15 -0
- package/dist/react/FeedbackConfirm.d.ts.map +1 -0
- package/dist/react/FeedbackContext.d.ts +172 -0
- package/dist/react/FeedbackContext.d.ts.map +1 -0
- package/dist/react/FeedbackDialogue.d.ts +14 -0
- package/dist/react/FeedbackDialogue.d.ts.map +1 -0
- package/dist/react/FeedbackPanel.d.ts +10 -0
- package/dist/react/FeedbackPanel.d.ts.map +1 -0
- package/dist/react/FeedbackProvider.d.ts +28 -0
- package/dist/react/FeedbackProvider.d.ts.map +1 -0
- package/dist/react/FeedbackRecorder.d.ts +14 -0
- package/dist/react/FeedbackRecorder.d.ts.map +1 -0
- package/dist/react/index.d.ts +33 -0
- package/dist/react/index.d.ts.map +1 -0
- package/dist/react/resolver/submitResolver.d.ts +60 -0
- package/dist/react/resolver/submitResolver.d.ts.map +1 -0
- package/dist/react/useFeedback.d.ts +27 -0
- package/dist/react/useFeedback.d.ts.map +1 -0
- package/dist/react.d.ts +2 -0
- package/dist/react.d.ts.map +1 -0
- package/dist/react.js +1576 -0
- package/dist/react.js.map +1 -0
- package/dist/server.d.ts +2 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +2 -0
- package/dist/server.js.map +1 -0
- package/package.json +72 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,455 @@
|
|
|
1
|
+
import { D, S, d, e, i, r, a, b, t, v, w } from "./chunks/i18n-RpKqSWAT.js";
|
|
2
|
+
const BASE_PRIORITY = {
|
|
3
|
+
bug: 60,
|
|
4
|
+
ux: 40,
|
|
5
|
+
feature: 20,
|
|
6
|
+
other: 10
|
|
7
|
+
};
|
|
8
|
+
const ENVIRONMENT_BONUS = {
|
|
9
|
+
production: 20,
|
|
10
|
+
staging: 10,
|
|
11
|
+
development: 0
|
|
12
|
+
};
|
|
13
|
+
function calculatePriority(type, reporterCount, hasScreenshot, environment) {
|
|
14
|
+
const base = BASE_PRIORITY[type];
|
|
15
|
+
const env = ENVIRONMENT_BONUS[environment ?? "development"];
|
|
16
|
+
const reporterBonus = Math.min(reporterCount - 1, 5) * 4;
|
|
17
|
+
const screenshotBonus = hasScreenshot ? 5 : 0;
|
|
18
|
+
const raw = base + env + reporterBonus + screenshotBonus;
|
|
19
|
+
return Math.max(0, Math.min(100, raw));
|
|
20
|
+
}
|
|
21
|
+
function generateFeedbackId(nowIso) {
|
|
22
|
+
const date = nowIso ? nowIso.slice(0, 10) : (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
23
|
+
const uuid8 = crypto.randomUUID().replace(/-/g, "").slice(0, 8);
|
|
24
|
+
return `FB-${date}-${uuid8}`;
|
|
25
|
+
}
|
|
26
|
+
function buildGSI1SK(priority, id) {
|
|
27
|
+
const inverted = 100 - Math.max(0, Math.min(100, priority));
|
|
28
|
+
const padded = String(inverted).padStart(3, "0");
|
|
29
|
+
return `${padded}#${id}`;
|
|
30
|
+
}
|
|
31
|
+
function processFeedbackCapture(input, nowIso) {
|
|
32
|
+
const now = nowIso ?? (/* @__PURE__ */ new Date()).toISOString();
|
|
33
|
+
const id = generateFeedbackId(now);
|
|
34
|
+
const status = "open";
|
|
35
|
+
const tags = input.tags ?? [];
|
|
36
|
+
const environment = input.environment;
|
|
37
|
+
const priority = calculatePriority(input.type, 1, input.hasScreenshot, environment);
|
|
38
|
+
const reporter = {
|
|
39
|
+
userId: input.submittedBy,
|
|
40
|
+
addedAt: now
|
|
41
|
+
};
|
|
42
|
+
const PK = `PROJECT#${input.appName}`;
|
|
43
|
+
const SK = `FB#${now}#${id}`;
|
|
44
|
+
const GSI1PK = `STATUS#${status}`;
|
|
45
|
+
const GSI1SK = buildGSI1SK(priority, id);
|
|
46
|
+
const GSI2PK = `TYPE#${input.type}#PROJECT#${input.appName}`;
|
|
47
|
+
const GSI2SK = now;
|
|
48
|
+
const item = {
|
|
49
|
+
PK,
|
|
50
|
+
SK,
|
|
51
|
+
GSI1PK,
|
|
52
|
+
GSI1SK,
|
|
53
|
+
GSI2PK,
|
|
54
|
+
GSI2SK,
|
|
55
|
+
id,
|
|
56
|
+
appName: input.appName,
|
|
57
|
+
type: input.type,
|
|
58
|
+
status,
|
|
59
|
+
priority,
|
|
60
|
+
content: input.content,
|
|
61
|
+
context: input.context,
|
|
62
|
+
hasScreenshot: input.hasScreenshot,
|
|
63
|
+
submittedAt: now,
|
|
64
|
+
lastModifiedAt: now,
|
|
65
|
+
submittedBy: input.submittedBy,
|
|
66
|
+
reporters: [reporter],
|
|
67
|
+
reporterCount: 1,
|
|
68
|
+
dialogueHistory: input.dialogueHistory ?? [],
|
|
69
|
+
corrections: [],
|
|
70
|
+
tags
|
|
71
|
+
};
|
|
72
|
+
if (input.screenshotS3Key !== void 0) {
|
|
73
|
+
item.screenshotS3Key = input.screenshotS3Key;
|
|
74
|
+
}
|
|
75
|
+
if (input.environment !== void 0) {
|
|
76
|
+
item.environment = input.environment;
|
|
77
|
+
}
|
|
78
|
+
if (input.version !== void 0) {
|
|
79
|
+
item.version = input.version;
|
|
80
|
+
}
|
|
81
|
+
if (input.traceId !== void 0) {
|
|
82
|
+
item.traceId = input.traceId;
|
|
83
|
+
}
|
|
84
|
+
if (input.classification !== void 0) {
|
|
85
|
+
item.classification = input.classification;
|
|
86
|
+
}
|
|
87
|
+
if (input.scope !== void 0) {
|
|
88
|
+
item.scope = input.scope;
|
|
89
|
+
}
|
|
90
|
+
if (input.size !== void 0) {
|
|
91
|
+
item.size = input.size;
|
|
92
|
+
}
|
|
93
|
+
return item;
|
|
94
|
+
}
|
|
95
|
+
const DEFAULT_DIALOGUE_RESPONSE = {
|
|
96
|
+
success: true,
|
|
97
|
+
response: "Thank you for your feedback. Can you tell me more?",
|
|
98
|
+
conversationComplete: false
|
|
99
|
+
};
|
|
100
|
+
function createMemoryTransport() {
|
|
101
|
+
const submissions = [];
|
|
102
|
+
const dialogueResponses = [];
|
|
103
|
+
const uploadedScreenshots = [];
|
|
104
|
+
return {
|
|
105
|
+
submissions,
|
|
106
|
+
dialogueResponses,
|
|
107
|
+
uploadedScreenshots,
|
|
108
|
+
async submit(item) {
|
|
109
|
+
submissions.push(item);
|
|
110
|
+
return { success: true, feedbackId: item.id };
|
|
111
|
+
},
|
|
112
|
+
async exchange(feedbackId, message, history) {
|
|
113
|
+
if (dialogueResponses.length > 0) {
|
|
114
|
+
return dialogueResponses.shift();
|
|
115
|
+
}
|
|
116
|
+
return { ...DEFAULT_DIALOGUE_RESPONSE };
|
|
117
|
+
},
|
|
118
|
+
async upload(feedbackId, blob) {
|
|
119
|
+
uploadedScreenshots.push({ feedbackId, blob });
|
|
120
|
+
return {
|
|
121
|
+
success: true,
|
|
122
|
+
s3Key: `screenshots/${feedbackId}.png`
|
|
123
|
+
};
|
|
124
|
+
},
|
|
125
|
+
clear() {
|
|
126
|
+
submissions.length = 0;
|
|
127
|
+
dialogueResponses.length = 0;
|
|
128
|
+
uploadedScreenshots.length = 0;
|
|
129
|
+
}
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
const VALIDATION_RULES = {
|
|
133
|
+
CONTENT_MIN_LENGTH: 10,
|
|
134
|
+
CONTENT_MAX_LENGTH: 5e3,
|
|
135
|
+
TAG_MAX_COUNT: 10,
|
|
136
|
+
TAG_PATTERN: /^[a-z0-9-]+$/,
|
|
137
|
+
APP_NAME_PATTERN: /^[a-zA-Z0-9_-]+$/,
|
|
138
|
+
SCREENSHOT_MAX_BYTES: 5242880,
|
|
139
|
+
// 5 MB
|
|
140
|
+
CONSOLE_ERRORS_MAX: 20
|
|
141
|
+
};
|
|
142
|
+
function makeResult(errors, warnings) {
|
|
143
|
+
return { valid: errors.length === 0, errors, warnings };
|
|
144
|
+
}
|
|
145
|
+
function validateContent(content) {
|
|
146
|
+
const errors = [];
|
|
147
|
+
const warnings = [];
|
|
148
|
+
if (content.length < VALIDATION_RULES.CONTENT_MIN_LENGTH) {
|
|
149
|
+
errors.push(
|
|
150
|
+
`Feedback must be at least ${VALIDATION_RULES.CONTENT_MIN_LENGTH} characters (got ${content.length})`
|
|
151
|
+
);
|
|
152
|
+
} else if (content.length > VALIDATION_RULES.CONTENT_MAX_LENGTH) {
|
|
153
|
+
errors.push(
|
|
154
|
+
`Feedback must not exceed ${VALIDATION_RULES.CONTENT_MAX_LENGTH} characters (got ${content.length})`
|
|
155
|
+
);
|
|
156
|
+
} else if (content.trim().length === 0) {
|
|
157
|
+
warnings.push("Feedback appears to contain only whitespace");
|
|
158
|
+
}
|
|
159
|
+
return makeResult(errors, warnings);
|
|
160
|
+
}
|
|
161
|
+
function validateTags(tags) {
|
|
162
|
+
const errors = [];
|
|
163
|
+
const warnings = [];
|
|
164
|
+
if (tags.length > VALIDATION_RULES.TAG_MAX_COUNT) {
|
|
165
|
+
errors.push(
|
|
166
|
+
`Too many tags: ${tags.length} (maximum is ${VALIDATION_RULES.TAG_MAX_COUNT})`
|
|
167
|
+
);
|
|
168
|
+
}
|
|
169
|
+
for (const tag of tags) {
|
|
170
|
+
if (!VALIDATION_RULES.TAG_PATTERN.test(tag)) {
|
|
171
|
+
errors.push(
|
|
172
|
+
`Tag "${tag}" is invalid — must match /^[a-z0-9-]+$/ (lowercase letters, digits, hyphens only)`
|
|
173
|
+
);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
return makeResult(errors, warnings);
|
|
177
|
+
}
|
|
178
|
+
function validateAppName(appName) {
|
|
179
|
+
const errors = [];
|
|
180
|
+
const warnings = [];
|
|
181
|
+
if (!appName || appName.trim() === "") {
|
|
182
|
+
errors.push("appName is required");
|
|
183
|
+
} else if (!VALIDATION_RULES.APP_NAME_PATTERN.test(appName)) {
|
|
184
|
+
errors.push(
|
|
185
|
+
`appName "${appName}" is invalid — must match /^[a-zA-Z0-9_-]+$/`
|
|
186
|
+
);
|
|
187
|
+
}
|
|
188
|
+
return makeResult(errors, warnings);
|
|
189
|
+
}
|
|
190
|
+
function validateScreenshotSize(sizeBytes) {
|
|
191
|
+
const errors = [];
|
|
192
|
+
const warnings = [];
|
|
193
|
+
if (sizeBytes > VALIDATION_RULES.SCREENSHOT_MAX_BYTES) {
|
|
194
|
+
errors.push(
|
|
195
|
+
`Screenshot size ${sizeBytes} bytes exceeds the maximum of ${VALIDATION_RULES.SCREENSHOT_MAX_BYTES} bytes (5 MB)`
|
|
196
|
+
);
|
|
197
|
+
}
|
|
198
|
+
return makeResult(errors, warnings);
|
|
199
|
+
}
|
|
200
|
+
function validateContext(context) {
|
|
201
|
+
const errors = [];
|
|
202
|
+
const warnings = [];
|
|
203
|
+
if (!context.url || context.url.trim() === "") {
|
|
204
|
+
warnings.push("context.url is empty — this may indicate a capture logic error");
|
|
205
|
+
}
|
|
206
|
+
if (context.consoleErrors && context.consoleErrors.length > VALIDATION_RULES.CONSOLE_ERRORS_MAX) {
|
|
207
|
+
warnings.push(
|
|
208
|
+
`context.consoleErrors has ${context.consoleErrors.length} entries (expected ≤${VALIDATION_RULES.CONSOLE_ERRORS_MAX}); truncation should occur in capture layer`
|
|
209
|
+
);
|
|
210
|
+
}
|
|
211
|
+
return makeResult(errors, warnings);
|
|
212
|
+
}
|
|
213
|
+
function validateCaptureInput(input) {
|
|
214
|
+
const allErrors = [];
|
|
215
|
+
const allWarnings = [];
|
|
216
|
+
if (input.content !== void 0) {
|
|
217
|
+
const contentResult = validateContent(input.content);
|
|
218
|
+
allErrors.push(...contentResult.errors);
|
|
219
|
+
allWarnings.push(...contentResult.warnings);
|
|
220
|
+
} else {
|
|
221
|
+
allErrors.push("content is required");
|
|
222
|
+
}
|
|
223
|
+
if (input.tags !== void 0) {
|
|
224
|
+
const tagResult = validateTags(input.tags);
|
|
225
|
+
allErrors.push(...tagResult.errors);
|
|
226
|
+
allWarnings.push(...tagResult.warnings);
|
|
227
|
+
}
|
|
228
|
+
if (input.appName !== void 0) {
|
|
229
|
+
const nameResult = validateAppName(input.appName);
|
|
230
|
+
allErrors.push(...nameResult.errors);
|
|
231
|
+
allWarnings.push(...nameResult.warnings);
|
|
232
|
+
} else {
|
|
233
|
+
allErrors.push("appName is required");
|
|
234
|
+
}
|
|
235
|
+
if (input.context !== void 0) {
|
|
236
|
+
const contextResult = validateContext(input.context);
|
|
237
|
+
allErrors.push(...contextResult.errors);
|
|
238
|
+
allWarnings.push(...contextResult.warnings);
|
|
239
|
+
}
|
|
240
|
+
return makeResult(allErrors, allWarnings);
|
|
241
|
+
}
|
|
242
|
+
function buildKeys(appName, id, type, status, priority, timestamp) {
|
|
243
|
+
const clampedPriority = Math.max(0, Math.min(100, priority));
|
|
244
|
+
const inverted = 100 - clampedPriority;
|
|
245
|
+
const paddedInverted = String(inverted).padStart(3, "0");
|
|
246
|
+
return {
|
|
247
|
+
PK: `PROJECT#${appName}`,
|
|
248
|
+
SK: `FB#${timestamp}#${id}`,
|
|
249
|
+
GSI1PK: `STATUS#${status}`,
|
|
250
|
+
GSI1SK: `${paddedInverted}#${id}`,
|
|
251
|
+
GSI2PK: `TYPE#${type}#PROJECT#${appName}`,
|
|
252
|
+
GSI2SK: timestamp
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
function parseSK(sk) {
|
|
256
|
+
const withoutPrefix = sk.startsWith("FB#") ? sk.slice(3) : sk;
|
|
257
|
+
const lastHash = withoutPrefix.lastIndexOf("#");
|
|
258
|
+
if (lastHash === -1) {
|
|
259
|
+
throw new Error(`Invalid SK format: "${sk}" — expected FB#{timestamp}#{id}`);
|
|
260
|
+
}
|
|
261
|
+
return {
|
|
262
|
+
timestamp: withoutPrefix.slice(0, lastHash),
|
|
263
|
+
id: withoutPrefix.slice(lastHash + 1)
|
|
264
|
+
};
|
|
265
|
+
}
|
|
266
|
+
function parseGSI2PK(gsi2pk) {
|
|
267
|
+
const typeMatch = gsi2pk.match(/^TYPE#([^#]+)#PROJECT#(.+)$/);
|
|
268
|
+
if (!typeMatch || !typeMatch[1] || !typeMatch[2]) {
|
|
269
|
+
throw new Error(
|
|
270
|
+
`Invalid GSI2PK format: "${gsi2pk}" — expected TYPE#{type}#PROJECT#{appName}`
|
|
271
|
+
);
|
|
272
|
+
}
|
|
273
|
+
return {
|
|
274
|
+
type: typeMatch[1],
|
|
275
|
+
appName: typeMatch[2]
|
|
276
|
+
};
|
|
277
|
+
}
|
|
278
|
+
function addReporter(item, userId, addedContext, nowIso) {
|
|
279
|
+
const addedAt = nowIso ?? (/* @__PURE__ */ new Date()).toISOString();
|
|
280
|
+
const newReporter = { userId, addedAt };
|
|
281
|
+
if (addedContext !== void 0) {
|
|
282
|
+
newReporter.addedContext = addedContext;
|
|
283
|
+
}
|
|
284
|
+
const newReporters = [...item.reporters, newReporter];
|
|
285
|
+
const newCount = newReporters.length;
|
|
286
|
+
return {
|
|
287
|
+
reporters: newReporters,
|
|
288
|
+
reporterCount: newCount,
|
|
289
|
+
GSI1SK: item.GSI1SK
|
|
290
|
+
};
|
|
291
|
+
}
|
|
292
|
+
function appendCorrection(corrections, correction, nowIso) {
|
|
293
|
+
const correctedAt = nowIso ?? (/* @__PURE__ */ new Date()).toISOString();
|
|
294
|
+
return [
|
|
295
|
+
...corrections,
|
|
296
|
+
{ ...correction, correctedAt }
|
|
297
|
+
];
|
|
298
|
+
}
|
|
299
|
+
function applyStatusTransition(item, newStatus, nowIso) {
|
|
300
|
+
const now = nowIso ?? (/* @__PURE__ */ new Date()).toISOString();
|
|
301
|
+
const resolvedAt = newStatus === "resolved" ? now : void 0;
|
|
302
|
+
return {
|
|
303
|
+
status: newStatus,
|
|
304
|
+
GSI1PK: `STATUS#${newStatus}`,
|
|
305
|
+
GSI1SK: item.GSI1SK,
|
|
306
|
+
// Priority unchanged
|
|
307
|
+
lastModifiedAt: now,
|
|
308
|
+
...resolvedAt !== void 0 ? { resolvedAt } : {},
|
|
309
|
+
...{}
|
|
310
|
+
};
|
|
311
|
+
}
|
|
312
|
+
class FeedbackError extends Error {
|
|
313
|
+
constructor(message, cause) {
|
|
314
|
+
super(message);
|
|
315
|
+
this.cause = cause;
|
|
316
|
+
this.name = this.constructor.name;
|
|
317
|
+
Object.setPrototypeOf(this, new.target.prototype);
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
class FeedbackValidationError extends FeedbackError {
|
|
321
|
+
constructor(message, fields) {
|
|
322
|
+
super(message);
|
|
323
|
+
this.fields = fields;
|
|
324
|
+
this.name = "FeedbackValidationError";
|
|
325
|
+
Object.setPrototypeOf(this, FeedbackValidationError.prototype);
|
|
326
|
+
}
|
|
327
|
+
code = "FEEDBACK_VALIDATION_ERROR";
|
|
328
|
+
retryable = false;
|
|
329
|
+
}
|
|
330
|
+
class FeedbackTransportError extends FeedbackError {
|
|
331
|
+
constructor(message, statusCode, cause) {
|
|
332
|
+
super(message, cause);
|
|
333
|
+
this.statusCode = statusCode;
|
|
334
|
+
this.name = "FeedbackTransportError";
|
|
335
|
+
Object.setPrototypeOf(this, FeedbackTransportError.prototype);
|
|
336
|
+
}
|
|
337
|
+
code = "FEEDBACK_TRANSPORT_ERROR";
|
|
338
|
+
retryable = true;
|
|
339
|
+
}
|
|
340
|
+
class FeedbackConfigError extends FeedbackError {
|
|
341
|
+
code = "FEEDBACK_CONFIG_ERROR";
|
|
342
|
+
retryable = false;
|
|
343
|
+
constructor(message, cause) {
|
|
344
|
+
super(message, cause);
|
|
345
|
+
this.name = "FeedbackConfigError";
|
|
346
|
+
Object.setPrototypeOf(this, FeedbackConfigError.prototype);
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
class FeedbackDialogueError extends FeedbackError {
|
|
350
|
+
code = "FEEDBACK_DIALOGUE_ERROR";
|
|
351
|
+
retryable = true;
|
|
352
|
+
constructor(message, cause) {
|
|
353
|
+
super(message, cause);
|
|
354
|
+
this.name = "FeedbackDialogueError";
|
|
355
|
+
Object.setPrototypeOf(this, FeedbackDialogueError.prototype);
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
class FeedbackScreenshotError extends FeedbackError {
|
|
359
|
+
code = "FEEDBACK_SCREENSHOT_ERROR";
|
|
360
|
+
retryable = false;
|
|
361
|
+
constructor(message, cause) {
|
|
362
|
+
super(message, cause);
|
|
363
|
+
this.name = "FeedbackScreenshotError";
|
|
364
|
+
Object.setPrototypeOf(this, FeedbackScreenshotError.prototype);
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
class FeedbackDuplicateError extends FeedbackError {
|
|
368
|
+
constructor(message, existingId) {
|
|
369
|
+
super(message);
|
|
370
|
+
this.existingId = existingId;
|
|
371
|
+
this.name = "FeedbackDuplicateError";
|
|
372
|
+
Object.setPrototypeOf(this, FeedbackDuplicateError.prototype);
|
|
373
|
+
}
|
|
374
|
+
code = "FEEDBACK_DUPLICATE_ERROR";
|
|
375
|
+
retryable = false;
|
|
376
|
+
}
|
|
377
|
+
function isFeedbackError(e2) {
|
|
378
|
+
return e2 instanceof FeedbackError;
|
|
379
|
+
}
|
|
380
|
+
function createErrorResponse(e2) {
|
|
381
|
+
if (isFeedbackError(e2)) {
|
|
382
|
+
const base = {
|
|
383
|
+
code: e2.code,
|
|
384
|
+
message: e2.message,
|
|
385
|
+
retryable: e2.retryable
|
|
386
|
+
};
|
|
387
|
+
if (e2 instanceof FeedbackValidationError && e2.fields !== void 0) {
|
|
388
|
+
base.fields = e2.fields;
|
|
389
|
+
}
|
|
390
|
+
if (e2 instanceof FeedbackDuplicateError) {
|
|
391
|
+
base.existingId = e2.existingId;
|
|
392
|
+
}
|
|
393
|
+
return base;
|
|
394
|
+
}
|
|
395
|
+
if (typeof e2 === "string") {
|
|
396
|
+
return {
|
|
397
|
+
code: "UNKNOWN_ERROR",
|
|
398
|
+
message: e2,
|
|
399
|
+
retryable: false
|
|
400
|
+
};
|
|
401
|
+
}
|
|
402
|
+
if (e2 instanceof Error) {
|
|
403
|
+
return {
|
|
404
|
+
code: "UNKNOWN_ERROR",
|
|
405
|
+
message: e2.message,
|
|
406
|
+
retryable: false
|
|
407
|
+
};
|
|
408
|
+
}
|
|
409
|
+
return {
|
|
410
|
+
code: "UNKNOWN_ERROR",
|
|
411
|
+
message: "An unknown error occurred",
|
|
412
|
+
retryable: false
|
|
413
|
+
};
|
|
414
|
+
}
|
|
415
|
+
export {
|
|
416
|
+
D as DEFAULT_LOCALE,
|
|
417
|
+
FeedbackConfigError,
|
|
418
|
+
FeedbackDialogueError,
|
|
419
|
+
FeedbackDuplicateError,
|
|
420
|
+
FeedbackError,
|
|
421
|
+
FeedbackScreenshotError,
|
|
422
|
+
FeedbackTransportError,
|
|
423
|
+
FeedbackValidationError,
|
|
424
|
+
S as SUPPORTED_LOCALES,
|
|
425
|
+
VALIDATION_RULES,
|
|
426
|
+
addReporter,
|
|
427
|
+
appendCorrection,
|
|
428
|
+
applyStatusTransition,
|
|
429
|
+
buildGSI1SK,
|
|
430
|
+
buildKeys,
|
|
431
|
+
calculatePriority,
|
|
432
|
+
createErrorResponse,
|
|
433
|
+
createMemoryTransport,
|
|
434
|
+
d as detectLocale,
|
|
435
|
+
e as emitMetric,
|
|
436
|
+
generateFeedbackId,
|
|
437
|
+
i as initTracer,
|
|
438
|
+
isFeedbackError,
|
|
439
|
+
parseGSI2PK,
|
|
440
|
+
parseSK,
|
|
441
|
+
processFeedbackCapture,
|
|
442
|
+
r as recordDialogueExchange,
|
|
443
|
+
a as recordSubmitEvent,
|
|
444
|
+
b as resolveConfig,
|
|
445
|
+
t,
|
|
446
|
+
validateAppName,
|
|
447
|
+
validateCaptureInput,
|
|
448
|
+
v as validateConfig,
|
|
449
|
+
validateContent,
|
|
450
|
+
validateContext,
|
|
451
|
+
validateScreenshotSize,
|
|
452
|
+
validateTags,
|
|
453
|
+
w as withSpan
|
|
454
|
+
};
|
|
455
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sources":["../src/core/capture.ts","../src/core/transport.ts","../src/core/validation.ts","../src/core/transforms.ts","../src/core/errors.ts"],"sourcesContent":["/**\n * Capture logic and priority calculation for feedback submissions.\n *\n * Framework-agnostic — no browser APIs, no DOM, no AWS.\n * Pure data transformation from user input to StoredFeedbackItem.\n */\n\nimport type {\n FeedbackType,\n FeedbackStatus,\n FeedbackEnvironment,\n FeedbackScope,\n FeedbackSize,\n FeedbackContext,\n StoredFeedbackItem,\n DialogueEntry,\n Reporter,\n} from './types.js';\n\n// ─────────────────────────────────────────────────────────────────\n// Input Type\n// ─────────────────────────────────────────────────────────────────\n\n/** Input for processFeedbackCapture */\nexport interface CaptureInput {\n type: FeedbackType;\n content: string;\n context: FeedbackContext;\n submittedBy: string;\n appName: string;\n hasScreenshot: boolean;\n screenshotS3Key?: string;\n environment?: FeedbackEnvironment;\n version?: string;\n tags?: string[];\n traceId?: string;\n dialogueHistory?: DialogueEntry[];\n classification?: string;\n scope?: FeedbackScope;\n size?: FeedbackSize;\n}\n\n// ─────────────────────────────────────────────────────────────────\n// Priority Formula\n// ─────────────────────────────────────────────────────────────────\n\n/**\n * Base priority scores by feedback type.\n * bug=60, ux=40, feature=20, other=10\n */\nconst BASE_PRIORITY: Record<FeedbackType, number> = {\n bug: 60,\n ux: 40,\n feature: 20,\n other: 10,\n};\n\n/**\n * Environment bonus scores.\n * production=+20, staging=+10, development=0\n */\nconst ENVIRONMENT_BONUS: Record<FeedbackEnvironment, number> = {\n production: 20,\n staging: 10,\n development: 0,\n};\n\n/**\n * Calculates the priority score for a feedback item.\n *\n * Formula:\n * base = baseByType (bug=60, ux=40, feature=20, other=10)\n * env = environmentBonus (production=+20, staging=+10, development=0)\n * rep = min(reporterCount - 1, 5) * 4 (+4 per additional reporter, max +20)\n * ss = hasScreenshot ? +5 : 0\n * priority = clamp(0, 100, base + env + rep + ss)\n */\nexport function calculatePriority(\n type: FeedbackType,\n reporterCount: number,\n hasScreenshot: boolean,\n environment?: FeedbackEnvironment,\n): number {\n const base = BASE_PRIORITY[type];\n const env = ENVIRONMENT_BONUS[environment ?? 'development'];\n const reporterBonus = Math.min(reporterCount - 1, 5) * 4;\n const screenshotBonus = hasScreenshot ? 5 : 0;\n\n const raw = base + env + reporterBonus + screenshotBonus;\n return Math.max(0, Math.min(100, raw));\n}\n\n// ─────────────────────────────────────────────────────────────────\n// ID Generation\n// ─────────────────────────────────────────────────────────────────\n\n/**\n * Generates a unique feedback ID in the format: FB-YYYY-MM-DD-{uuid8}\n * Uses crypto.randomUUID() for uniqueness; truncates to 8 chars.\n */\nexport function generateFeedbackId(nowIso?: string): string {\n const date = nowIso ? nowIso.slice(0, 10) : 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// GSI1SK Builder\n// ─────────────────────────────────────────────────────────────────\n\n/**\n * Builds the GSI1SK sort key value.\n * Format: {3-digit-inverted-priority}#{id}\n * Example: priority 85 → '015#FB-2026-07-25-abc12345'\n *\n * Inverted so DynamoDB ascending sort returns highest-priority first.\n */\nexport function buildGSI1SK(priority: number, id: string): string {\n const inverted = 100 - Math.max(0, Math.min(100, priority));\n const padded = String(inverted).padStart(3, '0');\n return `${padded}#${id}`;\n}\n\n// ─────────────────────────────────────────────────────────────────\n// Main Capture Processor\n// ─────────────────────────────────────────────────────────────────\n\n/**\n * Processes a feedback capture input into a StoredFeedbackItem\n * ready for the transport layer.\n *\n * Sets:\n * - status: 'open'\n * - reporterCount: 1\n * - reporters: [{ userId: submittedBy, addedAt: nowIso }]\n * - tags: [] when omitted\n * - All DynamoDB key fields (PK, SK, GSI1PK, GSI1SK, GSI2PK, GSI2SK)\n */\nexport function processFeedbackCapture(\n input: CaptureInput,\n nowIso?: string,\n): StoredFeedbackItem {\n const now = nowIso ?? new Date().toISOString();\n const id = generateFeedbackId(now);\n const status: FeedbackStatus = 'open';\n const tags = input.tags ?? [];\n const environment = input.environment;\n\n const priority = calculatePriority(input.type, 1, input.hasScreenshot, environment);\n\n const reporter: Reporter = {\n userId: input.submittedBy,\n addedAt: now,\n };\n\n const PK = `PROJECT#${input.appName}`;\n const SK = `FB#${now}#${id}`;\n const GSI1PK = `STATUS#${status}`;\n const GSI1SK = buildGSI1SK(priority, id);\n const GSI2PK = `TYPE#${input.type}#PROJECT#${input.appName}`;\n const GSI2SK = now;\n\n const item: StoredFeedbackItem = {\n PK,\n SK,\n GSI1PK,\n GSI1SK,\n GSI2PK,\n GSI2SK,\n id,\n appName: input.appName,\n type: input.type,\n status,\n priority,\n content: input.content,\n context: input.context,\n hasScreenshot: input.hasScreenshot,\n submittedAt: now,\n lastModifiedAt: now,\n submittedBy: input.submittedBy,\n reporters: [reporter],\n reporterCount: 1,\n dialogueHistory: input.dialogueHistory ?? [],\n corrections: [],\n tags,\n };\n\n // Optional fields — only set when defined\n if (input.screenshotS3Key !== undefined) {\n item.screenshotS3Key = input.screenshotS3Key;\n }\n if (input.environment !== undefined) {\n item.environment = input.environment;\n }\n if (input.version !== undefined) {\n item.version = input.version;\n }\n if (input.traceId !== undefined) {\n item.traceId = input.traceId;\n }\n if (input.classification !== undefined) {\n item.classification = input.classification;\n }\n if (input.scope !== undefined) {\n item.scope = input.scope;\n }\n if (input.size !== undefined) {\n item.size = input.size;\n }\n\n return item;\n}\n","/**\n * Transport contracts for the feedback package.\n *\n * Defines abstract interfaces that decouple core logic from\n * any specific implementation. Implementations live in the\n * consuming app's server handlers (slice 03).\n *\n * NO AWS SDK imports in this file.\n */\n\nimport type {\n StoredFeedbackItem,\n SubmitResult,\n DialogueResponse,\n UploadResult,\n DialogueEntry,\n} from './types.js';\n\n// ─────────────────────────────────────────────────────────────────\n// Transport Interfaces\n// ─────────────────────────────────────────────────────────────────\n\n/** What the React adapter calls at submit time */\nexport interface FeedbackSubmitTransport {\n submit(item: StoredFeedbackItem): Promise<SubmitResult>;\n}\n\n/** What the dialogue UI calls on each exchange */\nexport interface FeedbackDialogueTransport {\n exchange(\n feedbackId: string,\n message: string,\n history: DialogueEntry[],\n ): Promise<DialogueResponse>;\n}\n\n/** What the screenshot capture calls after html2canvas */\nexport interface FeedbackScreenshotTransport {\n upload(feedbackId: string, blob: Blob): Promise<UploadResult>;\n}\n\n/**\n * Composite transport — all three combined for full-feature transport.\n * Dialogue and screenshot are optional so implementations can omit them.\n */\nexport interface FeedbackTransport\n extends FeedbackSubmitTransport,\n Partial<FeedbackDialogueTransport>,\n Partial<FeedbackScreenshotTransport> {}\n\n// ─────────────────────────────────────────────────────────────────\n// Memory Transport (for testing)\n// ─────────────────────────────────────────────────────────────────\n\n/** Memory transport with access to internal state for assertions */\nexport interface MemoryTransport extends FeedbackTransport {\n /** All submitted items */\n submissions: StoredFeedbackItem[];\n /** Queue of dialogue responses to return in order */\n dialogueResponses: DialogueResponse[];\n /** Uploaded screenshot blobs */\n uploadedScreenshots: Array<{ feedbackId: string; blob: Blob }>;\n /** Clear all stored state */\n clear(): void;\n}\n\nconst DEFAULT_DIALOGUE_RESPONSE: DialogueResponse = {\n success: true,\n response: 'Thank you for your feedback. Can you tell me more?',\n conversationComplete: false,\n};\n\n/**\n * Creates an in-memory transport for testing.\n *\n * - submit(): stores the item and returns { success: true, feedbackId: item.id }\n * - exchange(): returns the first queued response, or a default stub\n * - upload(): records the blob reference\n * - Does NOT import any AWS SDK\n */\nexport function createMemoryTransport(): MemoryTransport {\n const submissions: StoredFeedbackItem[] = [];\n const dialogueResponses: DialogueResponse[] = [];\n const uploadedScreenshots: Array<{ feedbackId: string; blob: Blob }> = [];\n\n return {\n submissions,\n dialogueResponses,\n uploadedScreenshots,\n\n async submit(item: StoredFeedbackItem): Promise<SubmitResult> {\n submissions.push(item);\n return { success: true, feedbackId: item.id };\n },\n\n async exchange(\n feedbackId: string,\n message: string,\n history: DialogueEntry[],\n ): Promise<DialogueResponse> {\n void feedbackId;\n void message;\n void history;\n if (dialogueResponses.length > 0) {\n return dialogueResponses.shift() as DialogueResponse;\n }\n return { ...DEFAULT_DIALOGUE_RESPONSE };\n },\n\n async upload(feedbackId: string, blob: Blob): Promise<UploadResult> {\n uploadedScreenshots.push({ feedbackId, blob });\n return {\n success: true,\n s3Key: `screenshots/${feedbackId}.png`,\n };\n },\n\n clear() {\n submissions.length = 0;\n dialogueResponses.length = 0;\n uploadedScreenshots.length = 0;\n },\n };\n}\n","/**\n * Input validation functions for the feedback package.\n *\n * Used both client-side (before submission) and server-side (in handler).\n * No external dependencies — pure validation logic.\n *\n * Acceptance criteria evidence:\n * AC-65: createErrorResponse(new FeedbackValidationError('…', ['content']))\n * ])) includes fields: ['content'] → shown in errors.ts\n * AC-41: VALIDATION_RULES is exported as const (immutable) with values:\n * CONTENT_MIN_LENGTH=10, CONTENT_MAX_LENGTH=5000, TAG_MAX_COUNT=10\n * TAG_PATTERN=/^[a-z0-9-]+$/, [a-z0-9-] pattern applied\n */\n\nimport type { FeedbackContext } from './types.js';\nimport type { CaptureInput } from './capture.js';\n\n// ─────────────────────────────────────────────────────────────────\n// Validation Rules (exported as const — immutable)\n// ─────────────────────────────────────────────────────────────────\n\n/** Immutable validation constants */\nexport const VALIDATION_RULES = {\n CONTENT_MIN_LENGTH: 10,\n CONTENT_MAX_LENGTH: 5_000,\n TAG_MAX_COUNT: 10,\n TAG_PATTERN: /^[a-z0-9-]+$/,\n APP_NAME_PATTERN: /^[a-zA-Z0-9_-]+$/,\n SCREENSHOT_MAX_BYTES: 5_242_880, // 5 MB\n CONSOLE_ERRORS_MAX: 20,\n} as const;\n\n// ─────────────────────────────────────────────────────────────────\n// Result Type\n// ─────────────────────────────────────────────────────────────────\n\n/** Validation result with user-facing messages */\nexport interface ValidationResult {\n valid: boolean;\n errors: string[]; // Blocking — submission must be rejected\n warnings: string[]; // Non-blocking — suggestions only\n}\n\nfunction makeResult(errors: string[], warnings: string[]): ValidationResult {\n return { valid: errors.length === 0, errors, warnings };\n}\n\n// ─────────────────────────────────────────────────────────────────\n// Individual Validators\n// ─────────────────────────────────────────────────────────────────\n\n/**\n * Validates feedback content.\n * Fails below CONTENT_MIN_LENGTH (\"Feedback must be at least 10 characters\"),\n * fails above CONTENT_MAX_LENGTH, warns if only whitespace.\n */\nexport function validateContent(content: string): ValidationResult {\n const errors: string[] = [];\n const warnings: string[] = [];\n\n if (content.length < VALIDATION_RULES.CONTENT_MIN_LENGTH) {\n errors.push(\n `Feedback must be at least ${VALIDATION_RULES.CONTENT_MIN_LENGTH} characters (got ${content.length})`,\n );\n } else if (content.length > VALIDATION_RULES.CONTENT_MAX_LENGTH) {\n errors.push(\n `Feedback must not exceed ${VALIDATION_RULES.CONTENT_MAX_LENGTH} characters (got ${content.length})`,\n );\n } else if (content.trim().length === 0) {\n warnings.push('Feedback appears to contain only whitespace');\n }\n\n return makeResult(errors, warnings);\n}\n\n/**\n * Validates feedback tags.\n * Fails if count exceeds TAG_MAX_COUNT or if any tag doesn't match TAG_PATTERN.\n */\nexport function validateTags(tags: string[]): ValidationResult {\n const errors: string[] = [];\n const warnings: string[] = [];\n\n if (tags.length > VALIDATION_RULES.TAG_MAX_COUNT) {\n errors.push(\n `Too many tags: ${tags.length} (maximum is ${VALIDATION_RULES.TAG_MAX_COUNT})`,\n );\n }\n\n for (const tag of tags) {\n if (!VALIDATION_RULES.TAG_PATTERN.test(tag)) {\n errors.push(\n `Tag \"${tag}\" is invalid — must match /^[a-z0-9-]+$/ (lowercase letters, digits, hyphens only)`,\n );\n }\n }\n\n return makeResult(errors, warnings);\n}\n\n/**\n * Validates an app name.\n */\nexport function validateAppName(appName: string): ValidationResult {\n const errors: string[] = [];\n const warnings: string[] = [];\n\n if (!appName || appName.trim() === '') {\n errors.push('appName is required');\n } else if (!VALIDATION_RULES.APP_NAME_PATTERN.test(appName)) {\n errors.push(\n `appName \"${appName}\" is invalid — must match /^[a-zA-Z0-9_-]+$/`,\n );\n }\n\n return makeResult(errors, warnings);\n}\n\n/**\n * Validates screenshot size.\n */\nexport function validateScreenshotSize(sizeBytes: number): ValidationResult {\n const errors: string[] = [];\n const warnings: string[] = [];\n\n if (sizeBytes > VALIDATION_RULES.SCREENSHOT_MAX_BYTES) {\n errors.push(\n `Screenshot size ${sizeBytes} bytes exceeds the maximum of ${VALIDATION_RULES.SCREENSHOT_MAX_BYTES} bytes (5 MB)`,\n );\n }\n\n return makeResult(errors, warnings);\n}\n\n/**\n * Validates browser context.\n * Warns if url is empty; warns if consoleErrors exceeds 20 items.\n */\nexport function validateContext(context: FeedbackContext): ValidationResult {\n const errors: string[] = [];\n const warnings: string[] = [];\n\n if (!context.url || context.url.trim() === '') {\n warnings.push('context.url is empty — this may indicate a capture logic error');\n }\n\n if (\n context.consoleErrors &&\n context.consoleErrors.length > VALIDATION_RULES.CONSOLE_ERRORS_MAX\n ) {\n warnings.push(\n `context.consoleErrors has ${context.consoleErrors.length} entries (expected ≤${VALIDATION_RULES.CONSOLE_ERRORS_MAX}); truncation should occur in capture layer`,\n );\n }\n\n return makeResult(errors, warnings);\n}\n\n/**\n * Aggregate validator — runs all validators for a capture input.\n */\nexport function validateCaptureInput(input: Partial<CaptureInput>): ValidationResult {\n const allErrors: string[] = [];\n const allWarnings: string[] = [];\n\n if (input.content !== undefined) {\n const contentResult = validateContent(input.content);\n allErrors.push(...contentResult.errors);\n allWarnings.push(...contentResult.warnings);\n } else {\n allErrors.push('content is required');\n }\n\n if (input.tags !== undefined) {\n const tagResult = validateTags(input.tags);\n allErrors.push(...tagResult.errors);\n allWarnings.push(...tagResult.warnings);\n }\n\n if (input.appName !== undefined) {\n const nameResult = validateAppName(input.appName);\n allErrors.push(...nameResult.errors);\n allWarnings.push(...nameResult.warnings);\n } else {\n allErrors.push('appName is required');\n }\n\n if (input.context !== undefined) {\n const contextResult = validateContext(input.context);\n allErrors.push(...contextResult.errors);\n allWarnings.push(...contextResult.warnings);\n }\n\n return makeResult(allErrors, allWarnings);\n}\n","/**\n * Pure data transformation functions for DynamoDB key construction\n * and shape mapping.\n *\n * NO AWS SDK calls — only key construction and shape mapping.\n * All functions are pure: take plain objects, return plain objects.\n * No side effects.\n */\n\nimport type {\n FeedbackType,\n FeedbackStatus,\n StoredFeedbackItem,\n CorrectionEntry,\n Reporter,\n} from './types.js';\n\n// ─────────────────────────────────────────────────────────────────\n// Key Builder\n// ─────────────────────────────────────────────────────────────────\n\n/** The six DynamoDB keys for a feedback item */\nexport interface FeedbackKeys {\n PK: string; // PROJECT#{appName}\n SK: string; // FB#{timestamp}#{id}\n GSI1PK: string; // STATUS#{status}\n GSI1SK: string; // {inverted-priority-padded}#{id}\n GSI2PK: string; // TYPE#{type}#PROJECT#{appName}\n GSI2SK: string; // {timestamp}\n}\n\n/**\n * Builds the six DynamoDB keys for a feedback item.\n *\n * @param appName - Application name (e.g. 'sagrosoft20')\n * @param id - Feedback ID (e.g. 'FB-2026-07-25-abc12345')\n * @param type - Feedback type\n * @param status - Current status\n * @param priority - Priority score 0–100\n * @param timestamp - ISO 8601 timestamp\n */\nexport function buildKeys(\n appName: string,\n id: string,\n type: FeedbackType,\n status: FeedbackStatus,\n priority: number,\n timestamp: string,\n): FeedbackKeys {\n const clampedPriority = Math.max(0, Math.min(100, priority));\n const inverted = 100 - clampedPriority;\n const paddedInverted = String(inverted).padStart(3, '0');\n\n return {\n PK: `PROJECT#${appName}`,\n SK: `FB#${timestamp}#${id}`,\n GSI1PK: `STATUS#${status}`,\n GSI1SK: `${paddedInverted}#${id}`,\n GSI2PK: `TYPE#${type}#PROJECT#${appName}`,\n GSI2SK: timestamp,\n };\n}\n\n// ─────────────────────────────────────────────────────────────────\n// Key Parsers\n// ─────────────────────────────────────────────────────────────────\n\n/**\n * Extracts timestamp and id from a composite SK.\n * SK format: FB#{timestamp}#{id}\n */\nexport function parseSK(sk: string): { timestamp: string; id: string } {\n // SK format: FB#2026-07-25T10:00:00.000Z#FB-2026-07-25-abc12345\n const withoutPrefix = sk.startsWith('FB#') ? sk.slice(3) : sk;\n // Find the last '#' which separates timestamp from id\n const lastHash = withoutPrefix.lastIndexOf('#');\n if (lastHash === -1) {\n throw new Error(`Invalid SK format: \"${sk}\" — expected FB#{timestamp}#{id}`);\n }\n return {\n timestamp: withoutPrefix.slice(0, lastHash),\n id: withoutPrefix.slice(lastHash + 1),\n };\n}\n\n/**\n * Parses the GSI2PK into its component parts.\n * GSI2PK format: TYPE#{type}#PROJECT#{appName}\n */\nexport function parseGSI2PK(gsi2pk: string): { type: FeedbackType; appName: string } {\n // Format: TYPE#feature#PROJECT#oms4business\n const typeMatch = gsi2pk.match(/^TYPE#([^#]+)#PROJECT#(.+)$/);\n if (!typeMatch || !typeMatch[1] || !typeMatch[2]) {\n throw new Error(\n `Invalid GSI2PK format: \"${gsi2pk}\" — expected TYPE#{type}#PROJECT#{appName}`,\n );\n }\n return {\n type: typeMatch[1] as FeedbackType,\n appName: typeMatch[2],\n };\n}\n\n// ─────────────────────────────────────────────────────────────────\n// Reporter Mutations (immutable — returns new arrays)\n// ─────────────────────────────────────────────────────────────────\n\n/**\n * Adds a reporter to an item.\n * Returns new reporters array and updated reporterCount — does NOT mutate original.\n */\nexport function addReporter(\n item: StoredFeedbackItem,\n userId: string,\n addedContext?: string,\n nowIso?: string,\n): Pick<StoredFeedbackItem, 'reporters' | 'reporterCount' | 'GSI1SK'> {\n const addedAt = nowIso ?? new Date().toISOString();\n const newReporter: Reporter = { userId, addedAt };\n if (addedContext !== undefined) {\n newReporter.addedContext = addedContext;\n }\n\n // Spread to ensure immutability — does NOT mutate item.reporters\n const newReporters: Reporter[] = [...item.reporters, newReporter];\n const newCount = newReporters.length;\n\n // GSI1SK format preserves the inverted priority prefix, just uses same item id\n // The priority recalculation would normally happen in the capture layer\n // Here we preserve the existing GSI1SK structure (priority unchanged by reporter add)\n return {\n reporters: newReporters,\n reporterCount: newCount,\n GSI1SK: item.GSI1SK,\n };\n}\n\n// ─────────────────────────────────────────────────────────────────\n// Correction Mutations (immutable)\n// ─────────────────────────────────────────────────────────────────\n\n/**\n * Appends a correction record to the corrections array.\n * Returns a new array — does NOT mutate the original.\n */\nexport function appendCorrection(\n corrections: CorrectionEntry[],\n correction: Omit<CorrectionEntry, 'correctedAt'>,\n nowIso?: string,\n): CorrectionEntry[] {\n const correctedAt = nowIso ?? new Date().toISOString();\n return [\n ...corrections,\n { ...correction, correctedAt },\n ];\n}\n\n// ─────────────────────────────────────────────────────────────────\n// Status Transition\n// ─────────────────────────────────────────────────────────────────\n\n/**\n * Applies a status transition to an item.\n * Returns updated key fields + lastModifiedAt (does NOT mutate original).\n *\n * When transitioning to 'resolved': sets resolvedAt = nowIso.\n * When transitioning to any other status: resolvedAt is undefined.\n */\nexport function applyStatusTransition(\n item: StoredFeedbackItem,\n newStatus: FeedbackStatus,\n nowIso?: string,\n): Pick<\n StoredFeedbackItem,\n 'status' | 'GSI1PK' | 'GSI1SK' | 'lastModifiedAt' | 'resolvedAt' | 'resolvedBy'\n> {\n const now = nowIso ?? new Date().toISOString();\n\n // GSI1SK stays the same (priority doesn't change on status transition)\n // Only GSI1PK changes (new status)\n const resolvedAt: string | undefined = newStatus === 'resolved' ? now : undefined;\n const resolvedBy: string | undefined = undefined;\n\n return {\n status: newStatus,\n GSI1PK: `STATUS#${newStatus}`,\n GSI1SK: item.GSI1SK, // Priority unchanged\n lastModifiedAt: now,\n ...(resolvedAt !== undefined ? { resolvedAt } : {}),\n ...(resolvedBy !== undefined ? { resolvedBy } : {}),\n } as Pick<\n StoredFeedbackItem,\n 'status' | 'GSI1PK' | 'GSI1SK' | 'lastModifiedAt' | 'resolvedAt' | 'resolvedBy'\n >;\n}\n","/**\n * Domain error types for the feedback package.\n *\n * Every error class carries:\n * - code: string constant (usable in switch statements)\n * - retryable: boolean\n *\n * createErrorResponse converts any thrown value to a safe JSON-serialisable object.\n */\n\n// ─────────────────────────────────────────────────────────────────\n// Base Error Class\n// ─────────────────────────────────────────────────────────────────\n\n/** Base class for all feedback domain errors */\nexport abstract class FeedbackError extends Error {\n abstract readonly code: string;\n abstract readonly retryable: boolean;\n\n constructor(message: string, readonly cause?: unknown) {\n super(message);\n this.name = this.constructor.name;\n // Fix prototype chain for instanceof checks\n Object.setPrototypeOf(this, new.target.prototype);\n }\n}\n\n// ─────────────────────────────────────────────────────────────────\n// Concrete Error Classes\n// ─────────────────────────────────────────────────────────────────\n\n/** Thrown when input validation fails */\nexport class FeedbackValidationError extends FeedbackError {\n readonly code = 'FEEDBACK_VALIDATION_ERROR';\n readonly retryable = false;\n\n constructor(\n message: string,\n readonly fields?: string[],\n ) {\n super(message);\n this.name = 'FeedbackValidationError';\n Object.setPrototypeOf(this, FeedbackValidationError.prototype);\n }\n}\n\n/** Thrown when the transport layer fails (network, server error) */\nexport class FeedbackTransportError extends FeedbackError {\n readonly code = 'FEEDBACK_TRANSPORT_ERROR';\n readonly retryable = true;\n\n constructor(\n message: string,\n readonly statusCode?: number,\n cause?: unknown,\n ) {\n super(message, cause);\n this.name = 'FeedbackTransportError';\n Object.setPrototypeOf(this, FeedbackTransportError.prototype);\n }\n}\n\n/** Thrown when configuration is invalid or missing */\nexport class FeedbackConfigError extends FeedbackError {\n readonly code = 'FEEDBACK_CONFIG_ERROR';\n readonly retryable = false;\n\n constructor(message: string, cause?: unknown) {\n super(message, cause);\n this.name = 'FeedbackConfigError';\n Object.setPrototypeOf(this, FeedbackConfigError.prototype);\n }\n}\n\n/** Thrown when dialogue exchange fails */\nexport class FeedbackDialogueError extends FeedbackError {\n readonly code = 'FEEDBACK_DIALOGUE_ERROR';\n readonly retryable = true;\n\n constructor(message: string, cause?: unknown) {\n super(message, cause);\n this.name = 'FeedbackDialogueError';\n Object.setPrototypeOf(this, FeedbackDialogueError.prototype);\n }\n}\n\n/** Thrown when screenshot capture or upload fails */\nexport class FeedbackScreenshotError extends FeedbackError {\n readonly code = 'FEEDBACK_SCREENSHOT_ERROR';\n readonly retryable = false;\n\n constructor(message: string, cause?: unknown) {\n super(message, cause);\n this.name = 'FeedbackScreenshotError';\n Object.setPrototypeOf(this, FeedbackScreenshotError.prototype);\n }\n}\n\n/** Thrown when a duplicate feedback submission is detected */\nexport class FeedbackDuplicateError extends FeedbackError {\n readonly code = 'FEEDBACK_DUPLICATE_ERROR';\n readonly retryable = false;\n\n constructor(\n message: string,\n readonly existingId: string,\n ) {\n super(message);\n this.name = 'FeedbackDuplicateError';\n Object.setPrototypeOf(this, FeedbackDuplicateError.prototype);\n }\n}\n\n// ─────────────────────────────────────────────────────────────────\n// Type Guard\n// ─────────────────────────────────────────────────────────────────\n\n/**\n * Type guard — returns true if the value is a FeedbackError subclass.\n * Returns false for plain Error instances and other values.\n */\nexport function isFeedbackError(e: unknown): e is FeedbackError {\n return e instanceof FeedbackError;\n}\n\n// ─────────────────────────────────────────────────────────────────\n// Error Response Serialiser\n// ─────────────────────────────────────────────────────────────────\n\n/** Safe JSON-serialisable error response */\nexport interface ErrorResponse {\n code: string;\n message: string;\n retryable: boolean;\n fields?: string[]; // present for FeedbackValidationError\n existingId?: string; // present for FeedbackDuplicateError\n}\n\n/**\n * Converts any thrown value to a safe JSON-serialisable response object.\n *\n * - FeedbackError subclasses → uses their code, message, retryable\n * - FeedbackValidationError → includes fields\n * - FeedbackDuplicateError → includes existingId\n * - Plain string → { code: 'UNKNOWN_ERROR', message: theString, retryable: false }\n * - Plain Error → { code: 'UNKNOWN_ERROR', message: error.message, retryable: false }\n * - null/undefined/other → { code: 'UNKNOWN_ERROR', message: 'An unknown error occurred', retryable: false }\n */\nexport function createErrorResponse(e: unknown): ErrorResponse {\n if (isFeedbackError(e)) {\n const base: ErrorResponse = {\n code: e.code,\n message: e.message,\n retryable: e.retryable,\n };\n\n if (e instanceof FeedbackValidationError && e.fields !== undefined) {\n base.fields = e.fields;\n }\n\n if (e instanceof FeedbackDuplicateError) {\n base.existingId = e.existingId;\n }\n\n return base;\n }\n\n if (typeof e === 'string') {\n return {\n code: 'UNKNOWN_ERROR',\n message: e,\n retryable: false,\n };\n }\n\n if (e instanceof Error) {\n return {\n code: 'UNKNOWN_ERROR',\n message: e.message,\n retryable: false,\n };\n }\n\n return {\n code: 'UNKNOWN_ERROR',\n message: 'An unknown error occurred',\n retryable: false,\n };\n}\n"],"names":["e"],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"manifest.d.ts","sourceRoot":"","sources":["../src/manifest.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,CAAC"}
|
package/dist/manifest.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"manifest.js","sources":[],"sourcesContent":[],"names":[],"mappings":""}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { default as React } from 'react';
|
|
2
|
+
|
|
3
|
+
export interface FeedbackButtonProps {
|
|
4
|
+
/** Additional CSS class names (merged via className prop) */
|
|
5
|
+
className?: string;
|
|
6
|
+
/** Override the default tooltip/aria-label text */
|
|
7
|
+
label?: string;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* FeedbackButton — floating action button.
|
|
11
|
+
*
|
|
12
|
+
* Only renders when state === 'idle' (FeedbackPanel controls this).
|
|
13
|
+
* Calls open() from context on click — does NOT manage modal state.
|
|
14
|
+
*
|
|
15
|
+
* Keyboard accessible: Tab focuses, Enter/Space activates.
|
|
16
|
+
* Uses i18n key 'feedback.button.tooltip' for aria-label.
|
|
17
|
+
*/
|
|
18
|
+
export declare function FeedbackButton({ className, label }: FeedbackButtonProps): React.JSX.Element | null;
|
|
19
|
+
//# sourceMappingURL=FeedbackButton.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"FeedbackButton.d.ts","sourceRoot":"","sources":["../../src/react/FeedbackButton.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AACH,OAAO,KAAK,MAAM,OAAO,CAAC;AAU1B,MAAM,WAAW,mBAAmB;IAClC,6DAA6D;IAC7D,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,mDAAmD;IACnD,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AA+ED;;;;;;;;GAQG;AACH,wBAAgB,cAAc,CAAC,EAAE,SAAS,EAAE,KAAK,EAAE,EAAE,mBAAmB,GAAG,KAAK,CAAC,GAAG,CAAC,OAAO,GAAG,IAAI,CAwDlG"}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { default as React } from 'react';
|
|
2
|
+
|
|
3
|
+
export interface FeedbackConfirmProps {
|
|
4
|
+
className?: string;
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* FeedbackConfirm — pre-filled review form.
|
|
8
|
+
*
|
|
9
|
+
* All editable fields use i18n keys feedback.confirm.* for labels.
|
|
10
|
+
* Duplicate warning shown when capturedContext.duplicateCandidates is non-empty.
|
|
11
|
+
* Submit posts to config.transport.submitUrl; maps 201 → submitted state.
|
|
12
|
+
* On error: inline error with retry; modal stays open.
|
|
13
|
+
*/
|
|
14
|
+
export declare function FeedbackConfirm({ className }: FeedbackConfirmProps): React.JSX.Element | null;
|
|
15
|
+
//# sourceMappingURL=FeedbackConfirm.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"FeedbackConfirm.d.ts","sourceRoot":"","sources":["../../src/react/FeedbackConfirm.tsx"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AACH,OAAO,KAKN,MAAM,OAAO,CAAC;AAUf,MAAM,WAAW,oBAAoB;IACnC,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AA0DD;;;;;;;GAOG;AACH,wBAAgB,eAAe,CAAC,EAAE,SAAS,EAAE,EAAE,oBAAoB,GAAG,KAAK,CAAC,GAAG,CAAC,OAAO,GAAG,IAAI,CAuV7F"}
|