mirror of
https://dev.azure.com/hugendubel/ISA/_git/ISA-Frontend
synced 2025-12-31 09:37:15 +01:00
docs(logging): enhance API to support flexible context parameters
Update the logging library to accept context as either direct objects or functions (MaybeLoggerContextFn), providing better ergonomics while maintaining performance optimization through lazy evaluation. Changes: - Add MaybeLoggerContextFn type for flexible context handling - Update logger() factory to accept context as object or function - Update all LoggerApi methods to support both context formats - Enhance README with comprehensive examples and migration guide - Document performance benefits of function-based context - Add backward compatibility notes for v2.1.0 enhancement The new API is fully backward compatible - both direct objects and function wrappers work seamlessly.
This commit is contained in:
@@ -24,7 +24,8 @@ The Core Logging library provides a centralized logging service for the ISA Fron
|
||||
- **Multiple log levels** (trace, debug, info, warn, error, off)
|
||||
- **Flexible sink architecture** - Multiple output destinations (console, remote server, etc.)
|
||||
- **Hierarchical context system** - Component, instance, and message-level contexts
|
||||
- **Performance optimized** - Early filtering and lazy evaluation
|
||||
- **Flexible context handling** - Pass context as direct objects or functions for lazy evaluation
|
||||
- **Performance optimized** - Early filtering and lazy evaluation with minimal overhead
|
||||
- **Type-safe APIs** - Full TypeScript support with comprehensive interfaces
|
||||
- **Error resilience** - Logging failures don't affect application behavior
|
||||
- **Angular integration** - Native dependency injection support
|
||||
@@ -70,17 +71,25 @@ import { logger } from '@isa/core/logging';
|
||||
template: '...'
|
||||
})
|
||||
export class ExampleComponent {
|
||||
#logger = logger();
|
||||
|
||||
// Create logger with static context
|
||||
#logger = logger({ component: 'ExampleComponent' });
|
||||
|
||||
ngOnInit(): void {
|
||||
this.#logger.info('Component initialized');
|
||||
}
|
||||
|
||||
|
||||
handleUserAction(): void {
|
||||
this.#logger.debug('User action triggered', () => ({
|
||||
// Context as function (lazy evaluation - recommended for performance)
|
||||
this.#logger.debug('User action triggered', () => ({
|
||||
action: 'button-click',
|
||||
timestamp: Date.now()
|
||||
timestamp: Date.now()
|
||||
}));
|
||||
|
||||
// Context as object (simpler syntax)
|
||||
this.#logger.debug('User action completed', {
|
||||
action: 'button-click',
|
||||
result: 'success'
|
||||
});
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -184,14 +193,16 @@ The main interface for logging operations returned by the `logger()` factory:
|
||||
|
||||
```typescript
|
||||
interface LoggerApi {
|
||||
trace(message: string, context?: () => LoggerContext): void;
|
||||
debug(message: string, context?: () => LoggerContext): void;
|
||||
info(message: string, context?: () => LoggerContext): void;
|
||||
warn(message: string, context?: () => LoggerContext): void;
|
||||
error(message: string, error?: Error, context?: () => LoggerContext): void;
|
||||
trace(message: string, context?: MaybeLoggerContextFn): void;
|
||||
debug(message: string, context?: MaybeLoggerContextFn): void;
|
||||
info(message: string, context?: MaybeLoggerContextFn): void;
|
||||
warn(message: string, context?: MaybeLoggerContextFn): void;
|
||||
error(message: string, error?: Error, context?: MaybeLoggerContextFn): void;
|
||||
}
|
||||
```
|
||||
|
||||
**Note:** Context parameters accept either a context object directly or a function that returns a context object (`MaybeLoggerContextFn`), providing flexibility for both static and dynamic context scenarios.
|
||||
|
||||
#### `LoggerContext`
|
||||
|
||||
Type definition for context data passed to logging methods:
|
||||
@@ -200,8 +211,14 @@ Type definition for context data passed to logging methods:
|
||||
interface LoggerContext {
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
// Helper types for flexible context handling
|
||||
type LoggerContextFn = () => LoggerContext;
|
||||
type MaybeLoggerContextFn = LoggerContext | LoggerContextFn;
|
||||
```
|
||||
|
||||
The `MaybeLoggerContextFn` type allows you to pass context either as a direct object or as a function that returns context. This provides flexibility when you need dynamic context evaluation.
|
||||
|
||||
#### `Sink`
|
||||
|
||||
Interface for creating custom logging destinations:
|
||||
@@ -219,23 +236,29 @@ interface Sink {
|
||||
|
||||
### Factory Functions
|
||||
|
||||
#### `logger(ctxFn?: () => LoggerContext): LoggerApi`
|
||||
#### `logger(ctx?: MaybeLoggerContextFn): LoggerApi`
|
||||
|
||||
Creates a logger instance with optional dynamic context:
|
||||
Creates a logger instance with optional dynamic or static context:
|
||||
|
||||
```typescript
|
||||
// Basic usage
|
||||
const basicLogger = logger();
|
||||
|
||||
// With dynamic context
|
||||
const contextLogger = logger(() => ({
|
||||
// With dynamic context (function)
|
||||
const contextLogger = logger(() => ({
|
||||
userId: getCurrentUserId(),
|
||||
sessionId: getSessionId()
|
||||
sessionId: getSessionId()
|
||||
}));
|
||||
|
||||
// With static context (object)
|
||||
const staticLogger = logger({
|
||||
component: 'UserService',
|
||||
module: 'Authentication'
|
||||
});
|
||||
```
|
||||
|
||||
**Parameters:**
|
||||
- `ctxFn` (optional): Function that returns context data to be included with each log message
|
||||
- `ctx` (optional): Context data (object) or function that returns context data to be included with each log message
|
||||
|
||||
**Returns:** A `LoggerApi` instance configured with the specified context
|
||||
|
||||
@@ -491,17 +514,24 @@ export class ExampleComponent {
|
||||
|
||||
### Log Contexts
|
||||
|
||||
Contexts allow you to add structured data to your log messages:
|
||||
Contexts allow you to add structured data to your log messages. You can pass context either as a direct object or as a function:
|
||||
|
||||
```typescript
|
||||
// Add user information to logs
|
||||
this.#logger.info('User action completed', {
|
||||
// Add user information to logs - direct object
|
||||
this.#logger.info('User action completed', {
|
||||
userId: user.id,
|
||||
action: 'profile-update',
|
||||
duration: performance.now() - startTime
|
||||
});
|
||||
|
||||
// Log errors with context
|
||||
// Add user information to logs - function (lazy evaluation)
|
||||
this.#logger.info('User action completed', () => ({
|
||||
userId: user.id,
|
||||
action: 'profile-update',
|
||||
duration: performance.now() - startTime
|
||||
}));
|
||||
|
||||
// Log errors with context - direct object
|
||||
try {
|
||||
// Some operation
|
||||
} catch (error) {
|
||||
@@ -510,8 +540,21 @@ try {
|
||||
attemptNumber: 3
|
||||
});
|
||||
}
|
||||
|
||||
// Log errors with context - function (recommended for performance)
|
||||
try {
|
||||
// Some operation
|
||||
} catch (error) {
|
||||
this.#logger.error('Operation failed', error as Error, () => ({
|
||||
operationId: '12345',
|
||||
attemptNumber: 3,
|
||||
timestamp: Date.now()
|
||||
}));
|
||||
}
|
||||
```
|
||||
|
||||
**Performance Tip:** Using functions for context (`() => {...}`) enables lazy evaluation - the context object is only created if the log level passes the threshold, improving performance when logging is disabled or filtered out.
|
||||
|
||||
### Service-Level Logging
|
||||
|
||||
```typescript
|
||||
@@ -601,61 +644,88 @@ describe('MyService', () => {
|
||||
|
||||
## Migration Guide
|
||||
|
||||
### Recent API Enhancement (v2.1.0)
|
||||
|
||||
The logging library now supports **flexible context parameters** through the `MaybeLoggerContextFn` type. You can now pass context either as:
|
||||
- A direct object (for simple cases)
|
||||
- A function returning an object (for lazy evaluation and performance optimization)
|
||||
|
||||
This enhancement is **backward compatible** - both approaches work seamlessly:
|
||||
|
||||
```typescript
|
||||
// Both approaches are valid:
|
||||
logger.info('Message', { userId: '123' }); // Direct object ✅
|
||||
logger.info('Message', () => ({ userId: '123' })); // Function ✅
|
||||
```
|
||||
|
||||
**Benefits of using functions:**
|
||||
- **Performance**: Context is only evaluated if the log level passes the threshold
|
||||
- **Dynamic values**: Context can include values that change between creation and logging
|
||||
- **Null safety**: Avoid errors from accessing properties on potentially null/undefined objects
|
||||
|
||||
### Migrating from Previous Versions
|
||||
|
||||
#### Context Parameter Changes
|
||||
|
||||
**Old API:**
|
||||
**Old API (v1.x):**
|
||||
```typescript
|
||||
// Previous version - context as direct parameter
|
||||
logger.info('Message', { userId: '123' });
|
||||
logger.error('Error occurred', error, { operationId: '456' });
|
||||
```
|
||||
|
||||
**New API:**
|
||||
**New API (v2.x):**
|
||||
```typescript
|
||||
// Current version - context as function
|
||||
logger.info('Message', () => ({ userId: '123' }));
|
||||
logger.error('Error occurred', error, () => ({ operationId: '456' }));
|
||||
// Current version - context as function (recommended) or direct object (also supported)
|
||||
logger.info('Message', () => ({ userId: '123' })); // Recommended
|
||||
logger.info('Message', { userId: '123' }); // Also works
|
||||
|
||||
logger.error('Error occurred', error, () => ({ operationId: '456' })); // Recommended
|
||||
logger.error('Error occurred', error, { operationId: '456' }); // Also works
|
||||
```
|
||||
|
||||
#### Factory Function Updates
|
||||
|
||||
**Old API:**
|
||||
**Old API (v1.x):**
|
||||
```typescript
|
||||
// Previous version - no context support in factory
|
||||
const logger = logger();
|
||||
```
|
||||
|
||||
**New API:**
|
||||
**New API (v2.x):**
|
||||
```typescript
|
||||
// Current version - dynamic context support
|
||||
const logger = logger(() => ({ sessionId: getSessionId() }));
|
||||
// Current version - dynamic or static context support
|
||||
const logger = logger(() => ({ sessionId: getSessionId() })); // Dynamic
|
||||
const logger = logger({ component: 'UserService' }); // Static
|
||||
```
|
||||
|
||||
#### Configuration Changes
|
||||
|
||||
**Old API:**
|
||||
**Old API (v1.x):**
|
||||
```typescript
|
||||
// Previous version - direct service injection
|
||||
constructor(private loggingService: LoggingService) {}
|
||||
```
|
||||
|
||||
**New API:**
|
||||
**New API (v2.x):**
|
||||
```typescript
|
||||
// Current version - factory function
|
||||
#logger = logger();
|
||||
```
|
||||
|
||||
### Breaking Changes
|
||||
### Breaking Changes (v1.x → v2.x)
|
||||
|
||||
1. **Context parameters** now require function wrappers for lazy evaluation
|
||||
1. **Context parameters** support both direct objects and function wrappers (functions recommended for performance)
|
||||
2. **Error parameter** in LoggerApi is now optional instead of unknown type
|
||||
3. **Factory function** signature changed to support dynamic context
|
||||
3. **Factory function** signature changed to support dynamic context through `MaybeLoggerContextFn`
|
||||
|
||||
### Migration Steps
|
||||
### Migration Steps (v1.x → v2.x)
|
||||
|
||||
1. **Update context calls** - Wrap context objects in functions
|
||||
1. **Update context calls** - Optionally wrap context objects in functions for better performance
|
||||
2. **Replace service injection** - Use `logger()` factory instead of injecting `LoggingService`
|
||||
3. **Update error handling** - Error parameter is now optional in error() method
|
||||
4. **Add dynamic context** - Leverage new context functionality for better debugging
|
||||
|
||||
### Migration Steps (v2.0 → v2.1)
|
||||
|
||||
No breaking changes - the new `MaybeLoggerContextFn` type is backward compatible. Optionally update your code to use direct objects where lazy evaluation isn't needed.
|
||||
|
||||
@@ -1,149 +1,168 @@
|
||||
import { inject } from '@angular/core';
|
||||
import { LoggingService } from './logging.service';
|
||||
import { LoggerApi, LoggerContext } from './logging.types';
|
||||
import { LOGGER_CONTEXT } from './logging.providers';
|
||||
|
||||
/**
|
||||
* Factory function to create a logger instance with rich context support.
|
||||
* This is the primary way for components and services to access logging functionality.
|
||||
*
|
||||
* The logger supports hierarchical context through multiple mechanisms:
|
||||
* 1. Component-level context via provideLoggerContext()
|
||||
* 2. Instance-level context via the ctxFn parameter
|
||||
* 3. Message-level context via the logging methods
|
||||
*
|
||||
* These contexts are merged in order, with later contexts taking precedence.
|
||||
*
|
||||
* @param ctxFn - Optional function that returns dynamic context to be included with each log message
|
||||
* @returns A LoggerApi instance configured with the specified context
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* // Basic usage
|
||||
* const logger = logger();
|
||||
* logger.info('Simple message');
|
||||
*
|
||||
* // With dynamic context
|
||||
* const loggerWithContext = logger(() => ({
|
||||
* userId: getCurrentUserId(),
|
||||
* sessionId: getSessionId()
|
||||
* }));
|
||||
*
|
||||
* // Usage with component-level context
|
||||
* @Component({
|
||||
* providers: [
|
||||
* provideLoggerContext({ component: 'UserProfile' })
|
||||
* ]
|
||||
* })
|
||||
* class UserProfileComponent {
|
||||
* #logger = logger(() => ({ userId: this.currentUserId }));
|
||||
*
|
||||
* logAction(action: string) {
|
||||
* this.#logger.info('User action', { action }); // Includes all context levels
|
||||
* }
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
export function logger(ctxFn?: () => LoggerContext): LoggerApi {
|
||||
const loggingService = inject(LoggingService);
|
||||
|
||||
// Try to inject context if available
|
||||
const context =
|
||||
inject(LOGGER_CONTEXT, {
|
||||
optional: true,
|
||||
}) || [];
|
||||
|
||||
// Return an object with methods that forward to the logging service
|
||||
// with the provided context
|
||||
return {
|
||||
/**
|
||||
* Logs trace information with optional additional context.
|
||||
* @param message The log message.
|
||||
* @param additionalContext Optional context data specific to this log message.
|
||||
*/
|
||||
trace: (message: string, additionalContext?: () => LoggerContext): void => {
|
||||
loggingService.trace(message, () =>
|
||||
mergeContexts(context, ctxFn, additionalContext),
|
||||
);
|
||||
},
|
||||
/**
|
||||
* Logs debug information with optional additional context.
|
||||
* @param message The log message.
|
||||
* @param additionalContext Optional context data specific to this log message.
|
||||
*/
|
||||
debug: (message: string, additionalContext?: () => LoggerContext): void => {
|
||||
loggingService.debug(message, () =>
|
||||
mergeContexts(context, ctxFn, additionalContext),
|
||||
);
|
||||
},
|
||||
/**
|
||||
* Logs informational messages with optional additional context.
|
||||
* @param message The log message.
|
||||
* @param additionalContext Optional context data specific to this log message.
|
||||
*/
|
||||
info: (message: string, additionalContext?: () => LoggerContext): void => {
|
||||
loggingService.info(message, () =>
|
||||
mergeContexts(context, ctxFn, additionalContext),
|
||||
);
|
||||
},
|
||||
/**
|
||||
* Logs warning messages with optional additional context.
|
||||
* @param message The log message.
|
||||
* @param additionalContext Optional context data specific to this log message.
|
||||
*/
|
||||
warn: (message: string, additionalContext?: () => LoggerContext): void => {
|
||||
loggingService.warn(message, () =>
|
||||
mergeContexts(context, ctxFn, additionalContext),
|
||||
);
|
||||
},
|
||||
/**
|
||||
* Logs error messages with optional error object and additional context.
|
||||
* @param message The log message.
|
||||
* @param error Optional error object.
|
||||
* @param additionalContext Optional context data specific to this log message.
|
||||
*/
|
||||
error: (
|
||||
message: string,
|
||||
error?: Error,
|
||||
additionalContext?: () => LoggerContext,
|
||||
): void => {
|
||||
loggingService.error(message, error, () =>
|
||||
mergeContexts(context, ctxFn, additionalContext),
|
||||
);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Merges multiple levels of context into a single object.
|
||||
* Handles context priority and merging based on the logging system's hierarchy:
|
||||
* Component context -> Instance context -> Message context
|
||||
*
|
||||
* @param baseContext - Array of component-level contexts from provideLoggerContext
|
||||
* @param injectorContext - Optional function returning instance-level context
|
||||
* @param additionalContext - Message-specific context provided in the log call
|
||||
* @returns A merged context object containing properties from all provided contexts
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
function mergeContexts(
|
||||
baseContext: LoggerContext[] | null,
|
||||
injectorContext?: () => LoggerContext,
|
||||
additionalContext?: () => LoggerContext,
|
||||
): LoggerContext {
|
||||
const contextArray = Array.isArray(baseContext) ? baseContext : [];
|
||||
|
||||
const injectorCtx = injectorContext ? injectorContext() : {};
|
||||
|
||||
if (injectorCtx && typeof injectorCtx === 'object') {
|
||||
contextArray.push(injectorCtx);
|
||||
}
|
||||
|
||||
contextArray.push(additionalContext ? additionalContext() : {});
|
||||
|
||||
if (!contextArray.length) {
|
||||
return {};
|
||||
}
|
||||
|
||||
return contextArray.reduce((acc, context) => ({ ...acc, ...context }), {});
|
||||
}
|
||||
import { inject } from '@angular/core';
|
||||
import { LoggingService } from './logging.service';
|
||||
import {
|
||||
LoggerApi,
|
||||
LoggerContext,
|
||||
MaybeLoggerContextFn,
|
||||
} from './logging.types';
|
||||
import { LOGGER_CONTEXT } from './logging.providers';
|
||||
|
||||
/**
|
||||
* Factory function to create a logger instance with rich context support.
|
||||
* This is the primary way for components and services to access logging functionality.
|
||||
*
|
||||
* The logger supports hierarchical context through multiple mechanisms:
|
||||
* 1. Component-level context via provideLoggerContext()
|
||||
* 2. Instance-level context via the ctxFn parameter
|
||||
* 3. Message-level context via the logging methods
|
||||
*
|
||||
* These contexts are merged in order, with later contexts taking precedence.
|
||||
*
|
||||
* @param ctxFn - Optional function that returns dynamic context to be included with each log message
|
||||
* @returns A LoggerApi instance configured with the specified context
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* // Basic usage
|
||||
* const logger = logger();
|
||||
* logger.info('Simple message');
|
||||
*
|
||||
* // With dynamic context
|
||||
* const loggerWithContext = logger(() => ({
|
||||
* userId: getCurrentUserId(),
|
||||
* sessionId: getSessionId()
|
||||
* }));
|
||||
*
|
||||
* // Usage with component-level context
|
||||
* @Component({
|
||||
* providers: [
|
||||
* provideLoggerContext({ component: 'UserProfile' })
|
||||
* ]
|
||||
* })
|
||||
* class UserProfileComponent {
|
||||
* #logger = logger(() => ({ userId: this.currentUserId }));
|
||||
*
|
||||
* logAction(action: string) {
|
||||
* this.#logger.info('User action', { action }); // Includes all context levels
|
||||
* }
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
export function logger(ctx?: MaybeLoggerContextFn): LoggerApi {
|
||||
const loggingService = inject(LoggingService);
|
||||
|
||||
// Try to inject context if available
|
||||
const context =
|
||||
inject(LOGGER_CONTEXT, {
|
||||
optional: true,
|
||||
}) || [];
|
||||
|
||||
// Return an object with methods that forward to the logging service
|
||||
// with the provided context
|
||||
return {
|
||||
/**
|
||||
* Logs trace information with optional additional context.
|
||||
* @param message The log message.
|
||||
* @param additionalContext Optional context data specific to this log message.
|
||||
*/
|
||||
trace: (
|
||||
message: string,
|
||||
additionalContext?: MaybeLoggerContextFn,
|
||||
): void => {
|
||||
loggingService.trace(message, () =>
|
||||
mergeContexts(context, ctx, additionalContext),
|
||||
);
|
||||
},
|
||||
/**
|
||||
* Logs debug information with optional additional context.
|
||||
* @param message The log message.
|
||||
* @param additionalContext Optional context data specific to this log message.
|
||||
*/
|
||||
debug: (message: string, additionalContext?: () => LoggerContext): void => {
|
||||
loggingService.debug(message, () =>
|
||||
mergeContexts(context, ctx, additionalContext),
|
||||
);
|
||||
},
|
||||
/**
|
||||
* Logs informational messages with optional additional context.
|
||||
* @param message The log message.
|
||||
* @param additionalContext Optional context data specific to this log message.
|
||||
*/
|
||||
info: (message: string, additionalContext?: () => LoggerContext): void => {
|
||||
loggingService.info(message, () =>
|
||||
mergeContexts(context, ctx, additionalContext),
|
||||
);
|
||||
},
|
||||
/**
|
||||
* Logs warning messages with optional additional context.
|
||||
* @param message The log message.
|
||||
* @param additionalContext Optional context data specific to this log message.
|
||||
*/
|
||||
warn: (message: string, additionalContext?: () => LoggerContext): void => {
|
||||
loggingService.warn(message, () =>
|
||||
mergeContexts(context, ctx, additionalContext),
|
||||
);
|
||||
},
|
||||
/**
|
||||
* Logs error messages with optional error object and additional context.
|
||||
* @param message The log message.
|
||||
* @param error Optional error object.
|
||||
* @param additionalContext Optional context data specific to this log message.
|
||||
*/
|
||||
error: (
|
||||
message: string,
|
||||
error: Error,
|
||||
additionalContext?: () => LoggerContext,
|
||||
): void => {
|
||||
loggingService.error(message, error, () =>
|
||||
mergeContexts(context, ctx, additionalContext),
|
||||
);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Merges multiple levels of context into a single object.
|
||||
* Handles context priority and merging based on the logging system's hierarchy:
|
||||
* Component context -> Instance context -> Message context
|
||||
*
|
||||
* @param baseContext - Array of component-level contexts from provideLoggerContext
|
||||
* @param injectorContext - Optional function returning instance-level context
|
||||
* @param additionalContext - Message-specific context provided in the log call
|
||||
* @returns A merged context object containing properties from all provided contexts
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
function mergeContexts(
|
||||
baseContext: LoggerContext[] | null,
|
||||
injectorContext?: MaybeLoggerContextFn,
|
||||
additionalContext?: MaybeLoggerContextFn,
|
||||
): LoggerContext {
|
||||
const contextArray = Array.isArray(baseContext) ? baseContext : [];
|
||||
|
||||
const injectorCtx = injectorContext
|
||||
? typeof injectorContext === 'function'
|
||||
? injectorContext()
|
||||
: injectorContext
|
||||
: {};
|
||||
|
||||
if (injectorCtx && typeof injectorCtx === 'object') {
|
||||
contextArray.push(injectorCtx);
|
||||
}
|
||||
|
||||
const additionalCtx = additionalContext
|
||||
? typeof additionalContext === 'function'
|
||||
? additionalContext()
|
||||
: additionalContext
|
||||
: {};
|
||||
|
||||
if (additionalCtx && typeof additionalCtx === 'object') {
|
||||
contextArray.push(additionalCtx);
|
||||
}
|
||||
|
||||
if (!contextArray.length) {
|
||||
return {};
|
||||
}
|
||||
|
||||
return contextArray.reduce((acc, context) => ({ ...acc, ...context }), {});
|
||||
}
|
||||
|
||||
@@ -164,3 +164,7 @@ export interface LoggerApi {
|
||||
export interface LoggerContext {
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
export type LoggerContextFn = () => LoggerContext;
|
||||
|
||||
export type MaybeLoggerContextFn = LoggerContext | LoggerContextFn;
|
||||
|
||||
Reference in New Issue
Block a user