mirror of
https://github.com/rxliuli/apps.apple.com.git
synced 2025-11-10 00:30:32 +00:00
init commit
This commit is contained in:
67
shared/logger/src/base.ts
Normal file
67
shared/logger/src/base.ts
Normal 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;
|
||||
}
|
||||
92
shared/logger/src/composite.ts
Normal file
92
shared/logger/src/composite.ts
Normal 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 '';
|
||||
}
|
||||
}
|
||||
29
shared/logger/src/console.ts
Normal file
29
shared/logger/src/console.ts
Normal 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 '';
|
||||
}
|
||||
}
|
||||
93
shared/logger/src/errorkit/errorkit-logger.ts
Normal file
93
shared/logger/src/errorkit/errorkit-logger.ts
Normal 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 '';
|
||||
}
|
||||
}
|
||||
108
shared/logger/src/errorkit/errorkit.ts
Normal file
108
shared/logger/src/errorkit/errorkit.ts
Normal 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,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
31
shared/logger/src/index.ts
Normal file
31
shared/logger/src/index.ts
Normal 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);
|
||||
}
|
||||
122
shared/logger/src/local-storage-filter.ts
Normal file
122
shared/logger/src/local-storage-filter.ts
Normal 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;
|
||||
}
|
||||
Reference in New Issue
Block a user