Files
wehub-resource-sync 719032b19f
Update Schema / Update configuration json schema (push) Has been cancelled
Memory Benchmark / Memory Test (Full Analysis) (push) Has been cancelled
CI Quality / Lint GitHub Actions (actionlint) (push) Has been cancelled
CI Quality / Lint GitHub Actions (zizmor) (push) Has been cancelled
CI Quality / Check typos (push) Has been cancelled
CI / Test coverage (push) Has been cancelled
CI / Test (22.x, windows-latest) (push) Has been cancelled
CI / Test (24.x, macos-latest) (push) Has been cancelled
CI / Test (24.x, ubuntu-latest) (push) Has been cancelled
CI / Test (24.x, windows-latest) (push) Has been cancelled
CI / Test (26.x, macos-latest) (push) Has been cancelled
CI / Test (26.x, ubuntu-latest) (push) Has been cancelled
CI / Test (26.x, windows-latest) (push) Has been cancelled
CI / Test with Bun (latest, macos-latest) (push) Has been cancelled
CI / Test with Bun (latest, ubuntu-latest) (push) Has been cancelled
CI / Test with Bun (latest, windows-latest) (push) Has been cancelled
CI / Build and run (22.x, macos-latest) (push) Has been cancelled
CI / Build and run (22.x, ubuntu-latest) (push) Has been cancelled
CI / Build and run (22.x, windows-latest) (push) Has been cancelled
autofix.ci / autofix (push) Has been cancelled
CI Browser Extension / Lint Browser Extension (push) Has been cancelled
CI Browser Extension / Test Browser Extension (push) Has been cancelled
Memory Benchmark / Memory Test (push) Has been cancelled
CI Website / Lint Website Client (push) Has been cancelled
CI Website / Lint Website Server (push) Has been cancelled
CI Website / Test Website Server (push) Has been cancelled
CI Website / Bundle Website Server (push) Has been cancelled
CI / Lint Biome (push) Has been cancelled
CI / Lint oxlint (push) Has been cancelled
CI / Lint TypeScript (push) Has been cancelled
CI / Lint Secretlint (push) Has been cancelled
CI / Test (22.x, macos-latest) (push) Has been cancelled
CI / Test (22.x, ubuntu-latest) (push) Has been cancelled
CI / Build and run (24.x, macos-latest) (push) Has been cancelled
CI / Build and run (24.x, ubuntu-latest) (push) Has been cancelled
CI / Build and run (24.x, windows-latest) (push) Has been cancelled
CI / Build and run (26.x, macos-latest) (push) Has been cancelled
CI / Build and run (26.x, ubuntu-latest) (push) Has been cancelled
CI / Build and run (26.x, windows-latest) (push) Has been cancelled
CI / Build and run with Bun (latest, macos-latest) (push) Has been cancelled
CI / Build and run with Bun (latest, ubuntu-latest) (push) Has been cancelled
CI / Build and run with Bun (latest, windows-latest) (push) Has been cancelled
CodeQL / Analyze (javascript-typescript) (push) Has been cancelled
Docker / build (linux/amd64, ubuntu-latest) (push) Has been cancelled
Docker / build (linux/arm/v7, ubuntu-24.04-arm) (push) Has been cancelled
Docker / build (linux/arm64, ubuntu-24.04-arm) (push) Has been cancelled
Docker / merge (push) Has been cancelled
Pack repository with Repomix / pack-repo (push) Has been cancelled
Performance Benchmark History / Benchmark (macos-latest) (push) Has been cancelled
Performance Benchmark History / Benchmark (ubuntu-latest) (push) Has been cancelled
Performance Benchmark History / Benchmark (windows-latest) (push) Has been cancelled
Performance Benchmark History / Store Results (push) Has been cancelled
Test Repomix Action / Test Node.js 22 (push) Has been cancelled
Test Repomix Action / Test Node.js 24 (push) Has been cancelled
Test Repomix Action / Test Node.js 26 (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:39:37 +08:00

213 lines
6.4 KiB
TypeScript

// Analytics event categories
export const AnalyticsCategory = {
REPOSITORY: 'repository',
FORMAT: 'format',
OPTIONS: 'options',
OUTPUT: 'output',
} as const;
// Analytics event actions
export const AnalyticsAction = {
// Repository events
PACK_START: 'pack_start',
PACK_SUCCESS: 'pack_success',
PACK_SUCCESS_FILES: 'pack_success_files',
PACK_SUCCESS_CHARS: 'pack_success_chars',
PACK_ERROR: 'pack_error',
// Format events
FORMAT_CHANGE: 'format_change',
// Options events
TOGGLE_REMOVE_COMMENTS: 'toggle_remove_comments',
TOGGLE_REMOVE_EMPTY_LINES: 'toggle_remove_empty_lines',
TOGGLE_LINE_NUMBERS: 'toggle_line_numbers',
TOGGLE_FILE_SUMMARY: 'toggle_file_summary',
TOGGLE_DIRECTORY_STRUCTURE: 'toggle_directory_structure',
TOGGLE_OUTPUT_PARSABLE: 'toggle_output_parsable',
TOGGLE_COMPRESS: 'toggle_compress',
UPDATE_INCLUDE_PATTERNS: 'update_include_patterns',
UPDATE_IGNORE_PATTERNS: 'update_ignore_patterns',
// Output events
COPY_OUTPUT: 'copy_output',
DOWNLOAD_OUTPUT: 'download_output',
SHARE_OUTPUT: 'share_output',
} as const;
export type AnalyticsCategoryType = (typeof AnalyticsCategory)[keyof typeof AnalyticsCategory];
export type AnalyticsActionType = (typeof AnalyticsAction)[keyof typeof AnalyticsAction];
// Google Analytics event tracking interface
interface GAEventParams {
category: AnalyticsCategoryType;
action: AnalyticsActionType;
label?: string;
value?: number;
}
// Reduce a user-supplied repo identifier to a privacy-safe label.
// Strips query/hash/credentials and avoids forwarding raw URLs (which may
// contain tokens or private host names) into Google Analytics.
export function normalizeRepoLabel(input: string): string {
// Strip trailing slashes so `owner/repo/` still matches the shorthand
// regex below instead of falling through to URL parsing and returning
// `invalid`.
const trimmed = input?.trim().replace(/\/+$/, '') ?? '';
if (trimmed === '') return 'none';
// GitHub shorthand: owner/repo
if (/^[a-zA-Z0-9][\w.-]*\/[a-zA-Z0-9][\w.-]*$/.test(trimmed)) {
return `github:${trimmed.replace(/\.git$/, '')}`;
}
try {
const url = new URL(trimmed);
// Drop `www.` so `www.github.com/...` normalizes to the same `github:*`
// label as the canonical host.
const host = url.hostname.toLowerCase().replace(/^www\./, '');
const segments = url.pathname.split('/').filter(Boolean);
if (host === 'github.com' && segments.length >= 2) {
const repo = segments[1].replace(/\.git$/, '');
return `github:${segments[0]}/${repo}`;
}
if (host === 'gist.github.com' && segments.length >= 1) {
// Gist URLs come in two forms: `/<id>` (GitHub redirects to the user
// form) and `/<user>/<id>`. Handle both rather than leaking single-id
// URLs as `external:gist.github.com`.
if (segments.length === 1) return `gist:${segments[0]}`;
return `gist:${segments[0]}/${segments[1]}`;
}
return `external:${host}`;
} catch {
return 'invalid';
}
}
export type ErrorCategory =
| 'timeout'
| 'rate_limit'
| 'not_found'
| 'verification'
| 'invalid_input'
| 'network'
| 'server'
| 'unknown';
// Map a free-form error message to a stable category code so analytics
// labels stay bounded and do not echo server-supplied strings verbatim.
export function classifyError(message: string): ErrorCategory {
const lower = (message ?? '').toLowerCase();
if (lower.includes('timeout') || lower.includes('timed out')) return 'timeout';
if (lower.includes('rate limit') || lower.includes('429') || lower.includes('too many')) return 'rate_limit';
if (lower.includes('not found') || lower.includes('404')) return 'not_found';
if (lower.includes('turnstile') || lower.includes('verification') || lower.includes('verify')) {
return 'verification';
}
if (lower.includes('invalid') || lower.includes('format')) return 'invalid_input';
if (lower.includes('network') || lower.includes('fetch') || lower.includes('cors')) return 'network';
if (lower.includes('500') || lower.includes('502') || lower.includes('503') || lower.includes('server')) {
return 'server';
}
return 'unknown';
}
// Track an event using gtag
export function trackEvent({ category, action, label, value }: GAEventParams): void {
if (typeof window === 'undefined' || !window.gtag) {
return;
}
window.gtag('event', action, {
event_category: category,
event_label: label,
value: value,
});
}
// Analytics utility functions for specific events
export const analyticsUtils = {
// Repository events
trackPackStart(repoUrl: string): void {
trackEvent({
category: AnalyticsCategory.REPOSITORY,
action: AnalyticsAction.PACK_START,
label: normalizeRepoLabel(repoUrl),
});
},
trackPackSuccess(repoUrl: string, totalFiles: number, totalChars: number): void {
const label = normalizeRepoLabel(repoUrl);
trackEvent({
category: AnalyticsCategory.REPOSITORY,
action: AnalyticsAction.PACK_SUCCESS_FILES,
label,
value: totalFiles,
});
trackEvent({
category: AnalyticsCategory.REPOSITORY,
action: AnalyticsAction.PACK_SUCCESS_CHARS,
label,
value: totalChars,
});
},
trackPackError(repoUrl: string, error: string): void {
trackEvent({
category: AnalyticsCategory.REPOSITORY,
action: AnalyticsAction.PACK_ERROR,
label: `${normalizeRepoLabel(repoUrl)} - ${classifyError(error)}`,
});
},
// Options events
trackOptionToggle(action: AnalyticsActionType, enabled: boolean): void {
trackEvent({
category: AnalyticsCategory.OPTIONS,
action: action,
label: enabled ? 'enabled' : 'disabled',
});
},
// Output events
trackCopyOutput(format: string): void {
trackEvent({
category: AnalyticsCategory.OUTPUT,
action: AnalyticsAction.COPY_OUTPUT,
label: format,
});
},
trackDownloadOutput(format: string): void {
trackEvent({
category: AnalyticsCategory.OUTPUT,
action: AnalyticsAction.DOWNLOAD_OUTPUT,
label: format,
});
},
trackShareOutput(format: string): void {
trackEvent({
category: AnalyticsCategory.OUTPUT,
action: AnalyticsAction.SHARE_OUTPUT,
label: format,
});
},
};
// Type definitions for window.gtag
declare global {
interface Window {
gtag: (
command: 'event',
action: string,
params: {
event_category: string;
event_label?: string;
value?: number;
},
) => void;
}
}