init commit

This commit is contained in:
rxliuli
2025-11-04 05:03:50 +08:00
commit bce557cc2d
1396 changed files with 172991 additions and 0 deletions

67
shared/logger/src/base.ts Normal file
View File

@@ -0,0 +1,67 @@
import type { Level, Logger } from './types';
export abstract class BaseLogger<Args extends unknown[] = unknown[]>
implements Logger<Args>
{
constructor(protected readonly name: string) {}
/**
* Log a debug level message.
* Appropriate for verbose logging that explains steps/details of the inner state of
* a code unit.
*
* Example uses include in a size-constrain datastructure, logging when the size
* exceeds the threshold and elements are removed, or in a virtual scrolling
* component logging when a scroll event causes a new page of elements to be loaded.
*
* @param args Arguments to log (same as console.debug)
* @return empty string (for use in brackets {} in svelte components)
*/
debug(...args: Args): string {
return this.log('debug', ...args);
}
/**
* Log an info level message.
* Appropriate for informational messages that may be relevant to consumers of a code
* unit.
*
* Example uses include a router logging when transitions occur or a button logging
* clicks.
*
* @param args Arguments to log (same as console.info)
* @return empty string (for use in brackets {} in svelte components)
*/
info(...args: Args): string {
return this.log('info', ...args);
}
/**
* Log a warn level message.
* Appropriate for situations where state has been (or likely will be) corrupted or
* invariants have been broken.
*
* Example uses include a data structure warning when it is used before being fully
* initialized.
*
* @param args Arguments to log (same as console.warn)
* @return empty string (for use in brackets {} in svelte components)
*/
warn(...args: Args): string {
return this.log('warn', ...args);
}
/**
* Log an error message.
* Appropriate for thrown errors or situations where the apps breaks or has to
* engage in fallback behavior to avoid a more catastrophic failure.
*
* @param args Arguments to log (same as console.error)
* @return empty string (for use in brackets {} in svelte components)
*/
error(...args: Args): string {
return this.log('error', ...args);
}
protected abstract log(method: Level, ...args: Args): string;
}

View File

@@ -0,0 +1,92 @@
import type { LoggerFactory, Logger } from './types';
export class CompositeLoggerFactory implements LoggerFactory {
private readonly factories: LoggerFactory[];
constructor(factories: LoggerFactory[]) {
this.factories = factories;
}
loggerFor(name: string): Logger {
return new CompositeLogger(
this.factories.map((factory) => factory.loggerFor(name)),
);
}
}
export class CompositeLogger implements Logger {
private readonly loggers: Logger[];
constructor(loggers: Logger[]) {
this.loggers = loggers;
}
/**
* Log a debug level message.
* Appropriate for verbose logging that explains steps/details of the inner state of
* a code unit.
*
* Example uses include in a size-constrain datastructure, logging when the size
* exceeds the threshold and elements are removed, or in a virtual scrolling
* component logging when a scroll event causes a new page of elements to be loaded.
*
* @param args Arguments to log (same as console.debug)
* @return empty string (for use in brackets {} in svelte components)
*/
debug(...args: unknown[]): string {
return this.callAll('debug', args);
}
/**
* Log an info level message.
* Appropriate for informational messages that may be relevant to consumers of a code
* unit.
*
* Example uses include a router logging when transitions occur or a button logging
* clicks.
*
* @param args Arguments to log (same as console.info)
* @return empty string (for use in brackets {} in svelte components)
*/
info(...args: unknown[]): string {
return this.callAll('info', args);
}
/**
* Log a warn level message.
* Appropriate for situations where state has been (or likely will be) corrupted or
* invariants have been broken.
*
* Example uses include a data structure warning when it is used before being fully
* initialized.
*
* @param args Arguments to log (same as console.warn)
* @return empty string (for use in brackets {} in svelte components)
*/
warn(...args: unknown[]): string {
return this.callAll('warn', args);
}
/**
* Log an error message.
* Appropriate for thrown errors or situations where the apps breaks or has to
* engage in fallback behavior to avoid a more catastrophic failure.
*
* @param args Arguments to log (same as console.error)
* @return empty string (for use in brackets {} in svelte components)
*/
error(...args: unknown[]): string {
return this.callAll('error', args);
}
private callAll(
method: 'debug' | 'info' | 'warn' | 'error',
args: unknown[],
): string {
for (const logger of this.loggers) {
logger[method](...args);
}
return '';
}
}

View File

@@ -0,0 +1,29 @@
import { BaseLogger } from './base';
import type { Level, LoggerFactory, Logger } from './types';
import { shouldLog } from './local-storage-filter';
export class ConsoleLoggerFactory implements LoggerFactory {
loggerFor(name: string): Logger {
return new ConsoleLogger(name);
}
}
export class ConsoleLogger extends BaseLogger {
protected log(method: Level, ...args: unknown[]): string {
if (!shouldLog(this.name, method)) {
return '';
}
const log = console[method];
const prefix = `[${this.name}]`;
const [firstArg, ...rest] = args;
if (typeof firstArg === 'string') {
log(`${prefix} ${firstArg}`, ...rest);
} else {
log(prefix, ...args);
}
return '';
}
}

View File

@@ -0,0 +1,93 @@
import type { ErrorHub, ValueOf } from './types';
import type { LoggerFactory, Logger } from '../types';
/**
* Determines the level of logs to send to sentry.
*
*/
export const ERROR_REPORT_LEVEL = {
error: 'error',
error_warn: 'error_warn',
} as const;
type ReportLevel = ValueOf<typeof ERROR_REPORT_LEVEL>;
export class ErrorKitLoggerFactory implements LoggerFactory {
private readonly errorKit: ErrorHub;
private readonly reportLevel: ReportLevel;
constructor(errorKit: ErrorHub, reportLevel?: ReportLevel) {
this.errorKit = errorKit;
this.reportLevel = reportLevel ?? ERROR_REPORT_LEVEL.error;
}
loggerFor(name: string): Logger {
return new ErrorKitLogger(name, this.errorKit, this.reportLevel);
}
}
interface HasToString {
toString(): string;
}
export class ErrorKitLogger implements Logger {
private readonly name: string;
private readonly errorKit: ErrorHub;
private readonly reportLevel: ReportLevel;
constructor(name: string, errorKit: ErrorHub, reportLevel: ReportLevel) {
this.name = name;
this.errorKit = errorKit;
this.reportLevel = reportLevel;
}
private stringifyConsoleArgs(...args: unknown[]): string {
return args.reduce((acc: string, val: unknown) => {
let tempVal: HasToString;
switch (true) {
case val instanceof Error: {
tempVal = (val as unknown as InstanceType<typeof Error>)
.message;
break;
}
case typeof val === 'object': {
try {
tempVal = JSON.stringify(val);
} catch (e) {
tempVal = `failed to stringify ${val}`;
}
break;
}
case typeof val === 'undefined' || val === null: {
tempVal = `${val}`;
break;
}
default: {
tempVal = val as HasToString;
}
}
return `${acc} ${tempVal.toString()}`;
}, `[${this.name}]`) as string;
}
debug(..._args: unknown[]): string {
return '';
}
info(..._args: unknown[]): string {
return '';
}
warn(...args: unknown[]): string {
if (this.reportLevel === ERROR_REPORT_LEVEL.error_warn) {
this.errorKit.captureMessage(this.stringifyConsoleArgs(...args));
}
return '';
}
error(...args: unknown[]): string {
const errors = args.filter((item) => item instanceof Error) as Error[];
const message = this.stringifyConsoleArgs(...args);
const error = errors.length === 0 ? new Error(message) : errors[0];
error.message = message;
this.errorKit.captureException(error);
return '';
}
}

View File

@@ -0,0 +1,108 @@
import { Severity } from '@sentry/types';
import type { Logger, LoggerFactory } from '../types';
import type {
captureException,
captureMessage,
addBreadcrumb,
ErrorHub,
ErrorKitConfig,
} from './types';
type PartialSentryModule = {
captureException: typeof captureException;
captureMessage: typeof captureMessage;
addBreadcrumb: typeof addBreadcrumb;
};
export type ErrorKitInstance = InstanceType<typeof ErrorKit>;
export const setupErrorKit = (
config: ErrorKitConfig,
loggerFactory: LoggerFactory,
): ErrorKitInstance | undefined => {
if (typeof window === 'undefined') return;
const log = loggerFactory.loggerFor('errorkit');
const isMultiDev = window.location.href.includes('multidev');
const BUILD_ENV = process.env.NODE_ENV;
const isErrorKitEnabled = BUILD_ENV === 'production' && !isMultiDev;
const initializeErrorKit =
async (): Promise<PartialSentryModule | null> => {
let sentry: PartialSentryModule | null = null;
if (isErrorKitEnabled) {
try {
const { createSentryConfig } = await import(
'@amp-metrics/sentrykit'
);
const Sentry = await import('@sentry/browser');
Sentry.init(createSentryConfig(config));
sentry = {
addBreadcrumb: Sentry.addBreadcrumb,
captureException: Sentry.captureException,
captureMessage: Sentry.captureMessage,
};
} catch (e) {
log.error('something went wrong setting up errorKit', e);
}
}
return sentry;
};
return new ErrorKit(initializeErrorKit(), log, isErrorKitEnabled);
};
class ErrorKit implements ErrorHub {
private readonly sentry: Promise<PartialSentryModule | null>;
private readonly logger: Logger;
private readonly isErrorKitEnabled: boolean;
constructor(
sentry: Promise<PartialSentryModule | null>,
log: Logger,
isErrorKitEnabled: boolean,
) {
this.sentry = sentry;
this.logger = log;
this.isErrorKitEnabled = isErrorKitEnabled;
if (!isErrorKitEnabled) {
log.debug('errorkit is disabled');
}
}
async captureMessage(message: string) {
if (!this.isErrorKitEnabled) return;
const sentry = await this.sentry;
if (sentry) {
sentry.addBreadcrumb({
category: 'log.warn',
level: Severity.Warning,
});
sentry.captureMessage(message, Severity.Warning);
} else {
this.logger.warn(`${message} was not sent to errorKit`);
}
}
async captureException(exception: Error) {
if (!this.isErrorKitEnabled) return;
const sentry = await this.sentry;
if (sentry) {
sentry.addBreadcrumb({
type: 'error',
category: 'error',
level: Severity.Error,
});
sentry.captureException(exception);
} else {
this.logger.warn(
`The following exception was not sent to errorKit:`,
exception,
);
}
}
}

View File

@@ -0,0 +1,31 @@
import { getContext } from 'svelte';
import type { Logger, LoggerFactory } from './types';
export * from './composite';
export * from './console';
export * from './deferred';
export * from './recording';
export * from './sampled';
export * from './types';
export * from './void';
const CONTEXT_NAME = 'loggerFactory';
export function setContext(
context: Map<string, unknown>,
factory: LoggerFactory,
): void {
context.set(CONTEXT_NAME, factory);
}
export function loggerFor(subject: string): Logger {
const factory = getContext(CONTEXT_NAME) as LoggerFactory | undefined;
if (!factory) {
throw new Error(
'loggerFor called before setContext or outside of svelte component init',
);
}
return factory.loggerFor(subject);
}

View File

@@ -0,0 +1,122 @@
export type Level = 'debug' | 'info' | 'warn' | 'error';
// Numbers correspond to the levels above, with 0 meaning "no level"
type LevelNum = 4 | 3 | 2 | 1 | 0;
interface Rules {
named?: Record<string, LevelNum>;
defaultLevel?: LevelNum;
}
const LEVEL_TO_NUM: Record<Level | 'off' | '*' | '', LevelNum> = {
'*': 4,
debug: 4,
info: 3,
warn: 2,
error: 1,
off: 0,
'': 0,
};
/**
* Parses log filtering instructions from localStorage.onyxLog.
* The instructions are a series of comma separated directives that restrict
* logging. Restrictions indicate the highest log level that a named logger
* will emit. The name of the logger is the string passed to
* LoggerFactory.loggerFor.
*
* By default (ex. empty rule string), no logs will be emitted.
*
* The format of the directives is NAME=LEVEL. LEVEL can be one of:
*
* - * - all levels are logged (debug, info, warn, error)
* - debug - same as above
* - info - everything but debug is logged
* - warn - everything but info and debug is logged
* - error - only errors are logged
* - off (or empty string, ex. "MyClass=") - nothing will be logged
*
* Some examples:
*
* - '*=*' will emit all log levels from all loggers
* - '*=info,Foo=off' will emit everything but debug except or logs from
* the named logger Foo (which will be entirely suppressed)
* - 'Bar=error,Baz=warn' will emit errors from Bar and Baz and warnings from
* Baz
*
* NOTE: Keep this in sync with README.md!
*/
function parseRules(): Rules {
const onyxLog: string = (() => {
try {
// The typeof check is for SSR
return (
(typeof window !== 'undefined'
? window.localStorage.onyxLog
: '') || ''
);
} catch {
// window.localStorage will throw when referenced (at all) when
// Chrome has it disabled
// See: rdar://93367396 (Guard localStorage and sessionStorage use)
return '';
}
})();
const PRODUCTION_DEFAULT = {}; // no logs unless specified
const DEV_DEFAULT = {
defaultLevel: LEVEL_TO_NUM['*'], // All logs unless specified
};
const isDevelopment = (() => {
// This is a little tricky. The ENV var is not real. It's replaced by
// rollup-plugin-replace. Thus, we can't do the usual of testing for
// the existence of `process` and then doing `process?.env` etc.
// Instead, we just try the whole thing and try/catch. This way,
// rollup-plugin-replace sees that entire string verbatim and can
// replace it with the proper environment.
try {
// @ts-ignore
return process.env.NODE_ENV !== 'production';
} catch {
return false;
}
})();
const defaultRules = isDevelopment ? DEV_DEFAULT : PRODUCTION_DEFAULT;
// If the localStorage is specified, start from a clean slate. Otherwise,
// use the environment default
const rules: Rules = onyxLog.length > 0 ? {} : defaultRules;
for (const directive of onyxLog.split(',').filter((v) => v)) {
// Invalid directive, must be of the form 'name=level'
const parts = directive.split('=');
if (parts.length !== 2) {
continue;
}
const [name, maxLevelName] = parts;
const maxLevel =
LEVEL_TO_NUM[maxLevelName as keyof typeof LEVEL_TO_NUM];
// Invalid level
if (typeof maxLevel === 'undefined') {
continue;
}
if (name === '*') {
rules.defaultLevel = maxLevel;
} else {
rules.named = rules.named ?? {};
rules.named[name] = maxLevel;
}
}
return rules;
}
export function shouldLog(name: string, level: Level): boolean {
const rules = parseRules();
// Rules for the named logger take precedence over the default
const maxLevel = (rules.named || {})[name] ?? rules.defaultLevel ?? 0;
return LEVEL_TO_NUM[level] <= maxLevel;
}