Files
Kazuki Yamada fa06e5059c fix(website): Address PR review feedback on siteverify metric
Six items from gemini, claude initial review, and claude follow-up:

- turnstile.ts: Update misleading comment that claimed the metric filters
  on `event=turnstile_siteverify` and `outcome=success`. The actual
  Cloud Monitoring metrics in `monitoring/metrics/` filter on
  `siteverifyDurationMs` field presence, which uniformly captures both
  the parallel success log (event=turnstile_siteverify) and the four
  rejectAndLog failure paths (event=pack_completed). The comment
  contradicted README and YAML and would mislead future readers.
- turnstile.ts: Wrap rejectAndLog in a local `rejectWithDuration` helper
  so every post-siteverify branch automatically carries
  `siteverifyDurationMs`. Prevents drift if a fifth reject reason gets
  added later.
- client.ts: Split the wire-protocol `PackProgressStage` (server-emitted
  SSE values) from the display-only `DisplayProgressStage` superset that
  adds `verifying`. Keeping the synthetic stage out of the wire type
  prevents silent divergence with the server's `PackProgressStage`.
- usePackRequest.ts, TryItLoading.vue, TryItResult.vue: Switch the
  display-side type to `DisplayProgressStage`. `onProgress` callbacks
  still take the wire `PackProgressStage`.
- usePackRequest.ts: Clear `progressStage` on token-acquisition failure
  branches (aborted / error). Functionally invisible since loading=false
  hides the loading UI, but prevents the next submit's verifying flash
  from briefly showing the previous run's stale state.
- monitoring/metrics/turnstile_siteverify_duration.yaml: Retune the
  exponential bucket layout for the 100ms-1s SLO band where decisions
  get made. Doubling buckets only placed ~3 boundaries between 100ms
  and 1s; growthFactor=1.5 with scale=10 places ~8 boundaries there.
  18 finite buckets cover 10ms to ~9.85s, comfortably above the 5s
  siteverify timeout so timeouts don't land in overflow.
- monitoring/README.md: Document that pre-network rejections
  (secret_missing, missing_token, token_too_long) intentionally don't
  carry siteverifyDurationMs, so they're excluded from both metrics
  but still appear in the existing pack_requests metric.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 21:42:21 +09:00

181 lines
4.7 KiB
TypeScript

export interface PackOptions {
removeComments: boolean;
removeEmptyLines: boolean;
showLineNumbers: boolean;
fileSummary?: boolean;
directoryStructure?: boolean;
includePatterns?: string;
ignorePatterns?: string;
outputParsable?: boolean;
compress?: boolean;
}
export interface FileInfo {
path: string;
charCount: number;
selected?: boolean;
}
export interface PackRequest {
url: string;
format: 'xml' | 'markdown' | 'plain';
options: PackOptions;
file?: File;
}
export interface SuspiciousFile {
filePath: string;
messages: string[];
}
export interface PackResult {
content: string;
format: string;
metadata: {
repository: string;
timestamp: string;
summary: {
totalFiles: number;
totalCharacters: number;
totalTokens: number;
};
topFiles: {
path: string;
charCount: number;
tokenCount: number;
}[];
allFiles?: FileInfo[];
suspiciousFiles?: SuspiciousFile[];
};
}
export interface ErrorResponse {
error: string;
}
export class ApiError extends Error {
constructor(message: string) {
super(message);
this.name = 'ApiError';
}
}
// Wire-protocol stages — must stay aligned with the server-emitted SSE
// values (`website/server/src/types.ts:PackProgressStage`). `onProgress`
// callbacks receive only these, so any new server stage requires a
// deliberate type update on both sides.
export type PackProgressStage = 'cache-check' | 'cloning' | 'repository-fetch' | 'extracting' | 'processing';
// Display-only superset. `verifying` is a client-only synthetic stage
// shown while the server runs Turnstile siteverify (before any SSE event
// arrives); usePackRequest sets it locally between `takeToken()` returning
// and the first onProgress callback firing, so the loading UI shows a
// meaningful step instead of a generic "...". Keeping it out of
// `PackProgressStage` prevents the wire contract from drifting silently
// when display-only stages get added or renamed.
export type DisplayProgressStage = PackProgressStage | 'verifying';
export interface PackStreamCallbacks {
onProgress?: (stage: PackProgressStage, message?: string) => void;
signal?: AbortSignal;
}
const API_BASE_URL = import.meta.env.PROD ? 'https://api.repomix.com' : 'http://localhost:8080';
// NDJSON stream event types
interface ProgressEvent {
type: 'progress';
stage: PackProgressStage;
message?: string;
}
interface ResultEvent {
type: 'result';
data: PackResult;
}
interface StreamErrorEvent {
type: 'error';
message: string;
}
type StreamEvent = ProgressEvent | ResultEvent | StreamErrorEvent;
export async function packRepository(
request: PackRequest,
callbacks?: PackStreamCallbacks,
turnstileToken?: string,
): Promise<PackResult> {
const formData = new FormData();
if (request.file) {
formData.append('file', request.file);
} else {
formData.append('url', request.url);
}
formData.append('format', request.format);
formData.append('options', JSON.stringify(request.options));
// Token rides as a header rather than a form field to keep packRequestSchema
// free of cross-cutting concerns; the server-side turnstileMiddleware reads
// it before the schema validation runs.
const headers: HeadersInit = turnstileToken ? { 'X-Turnstile-Token': turnstileToken } : {};
const response = await fetch(`${API_BASE_URL}/api/pack`, {
method: 'POST',
headers,
body: formData,
signal: callbacks?.signal,
});
// Handle non-streaming error responses (validation errors return JSON)
if (!response.ok) {
const data = await response.json();
throw new ApiError((data as ErrorResponse).error);
}
// Handle NDJSON stream
if (!response.body) {
throw new ApiError('No response body received');
}
const reader = response.body.getReader();
const decoder = new TextDecoder();
let buffer = '';
let result: PackResult | null = null;
try {
while (true) {
const { value, done } = await reader.read();
if (done) break;
buffer += decoder.decode(value, { stream: true });
// Parse complete lines from buffer
const lines = buffer.split('\n');
buffer = lines.pop() || '';
for (const line of lines) {
if (!line.trim()) continue;
const event = JSON.parse(line) as StreamEvent;
if (event.type === 'progress') {
callbacks?.onProgress?.(event.stage, event.message);
} else if (event.type === 'result') {
result = event.data;
} else if (event.type === 'error') {
throw new ApiError(event.message);
}
}
}
} finally {
reader.releaseLock();
}
if (!result) {
throw new ApiError('No result received from server');
}
return result;
}