Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -86,5 +86,6 @@ Thumbs.db
.env.local

# Package specific
.cookies/
*.tar.gz
*.zip
13 changes: 13 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,3 +43,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Authentication strategy support.
- API request options customization.
- API envelope for structured responses.
# Unreleased

- Added compact structured `REQ`, `RES`, `ERR`, `RETRY`, `CACHE`, and `AUTH`
logging with total operation durations and network/cache source metadata.
- Added `ApiLoggingLevel`, immutable `ApiCallLogOptions`, typed log event
subclasses, callback logger support, and configurable body formatting.
- Added mandatory bounded redaction before custom logger callbacks, including
nested credentials, signed URLs, multipart uploads, binary data, streams,
long values, and cyclic objects.
- Added client-only per-request logging options without changing transport,
serialization, cache, retry, cookie, or authentication semantics.
- Preserved legacy `ApiLoggingConfig.enabled`, logging booleans, `ApiLogLevel`,
`ApiLogEvent`, and `ApiLogger` compatibility.
121 changes: 116 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,12 @@ await FlutterApiBridge.configure(
defaultTtl: Duration(minutes: 5),
),
retry: const ApiRetryConfig(maxAttempts: 3),
logging: const ApiLoggingConfig(
level: ApiLoggingLevel.basic,
showDuration: true,
showRequestId: false,
prettyPrintBodies: true,
),
clientIdentity: () => const ApiClientIdentity(
applicationName: 'RiderescueDriver',
applicationVersion: '2.4.1',
Expand Down Expand Up @@ -153,12 +159,14 @@ All requests accept common options for:
- idempotency keys;
- cache tag/path invalidation;
- operation IDs for diagnostics.
- client-only per-call logging controls.

```dart
const ApiRequestOptions(
headers: <String, String>{'X-Tenant': 'company-7'},
cookies: <String, String>{'preview': 'enabled'},
idempotencyKey: 'booking-create-123',
log: ApiCallLogOptions(enabled: false),
);
```

Expand All @@ -177,22 +185,125 @@ endpoint orchestration remains the generated package or application's concern.

## Logging

The bridge emits structured `ApiLogEvent` values with request IDs, operation
IDs, attempt numbers, durations, connection/session metadata, and cache/retry
events. Sensitive headers, cookies, tokens, passwords, secrets, OTP values,
and authentication codes are redacted.
The default logger emits compact, production-friendly summaries. Request and
response bodies, headers, query parameters, cookies, complete request IDs,
session IDs, and stack traces are not printed by default.

```text
REQ: user_auth.getCurrentAuthBootstrap · GET · /v1/user/auth/bootstrap
↳ attempt=1
RES: user_auth.getCurrentAuthBootstrap · GET · /v1/user/auth/bootstrap · 200 · 184ms
↳ source=network
ERR: booking.createBooking · POST · /v1/bookings · 422 · 197ms
↳ code=validation_failed · message="Vehicle is required"
RETRY: user_auth.refreshSession · attempt 2/3 · in 800ms
↳ reason=connection_timeout
CACHE: jobs.listAvailableJobs · hit
↳ source=disk · age=42s
AUTH: authenticated
↳ reason=credentials_restored
```

### Global configuration

`ApiLoggingLevel` controls package verbosity:

- `none`: no package logs;
- `errors`: terminal request and important authentication failures only;
- `basic`: concise request, response, error, retry, cache, and auth events;
- `detailed`: basic events plus metadata explicitly enabled below.

```dart
const ApiLoggingConfig(
level: ApiLoggingLevel.detailed,
logRequestHeaders: false,
logRequestBody: false,
logResponseHeaders: false,
logResponseBody: false,
logQueryParameters: false,
logCookies: false,
showDuration: true,
showRequestId: false,
prettyPrintBodies: true,
useAnsi: false,
);
```

`enabled: false` remains supported for compatibility. Prefer
`level: ApiLoggingLevel.none` in new code. `ApiLogLevel` remains the severity
attached to a structured event; it is separate from `ApiLoggingLevel`.

### Per-operation debugging

`ApiCallLogOptions` is immutable and stored only in `ApiRequestOptions`. The
shared caller reads it locally; it is never serialized into headers, query
parameters, request bodies, cookies, or DTOs.

```dart
const options = ApiRequestOptions(
log: ApiCallLogOptions(
requestBody: true,
responseBody: true,
),
);
```

Disable one operation without changing global configuration:

```dart
const options = ApiRequestOptions(
log: ApiCallLogOptions(enabled: false),
);
```

Generated packages should expose `ApiCallLogOptions? log` on each operation
and forward it into request options. Existing generated calls remain valid
because the argument is optional. Generator/template integration is tracked in
[issue #15](https://github.com/alidantech-org/flutter_api_bridge/issues/15);
this runtime repository intentionally does not patch generated operations.

### Redaction guarantees

Mandatory centralized redaction runs before every logger callback, including
custom sinks. Explicit body/header/cookie flags never bypass it. Matching is
case-insensitive across snake_case, kebab-case, and camelCase variants and
includes passwords, tokens, authorization, cookies, secrets, API keys, OTPs,
PINs, credentials, and signed URL parameters.

The redactor also:

- truncates long strings and large collections;
- limits recursive depth and detects cycles;
- strips URL user information and redacts sensitive query parameters;
- summarizes binary data and streams;
- reports multipart file counts and sizes without reading or printing bytes;
- prevents redaction or logger failures from affecting API requests.

Production applications should use `basic` or `errors`. Enable bodies only for
the smallest possible scope and disable them again after diagnosis.

### Custom sinks

The runtime emits typed `ApiRequestLogEvent`, `ApiResponseLogEvent`,
`ApiErrorLogEvent`, `ApiRetryLogEvent`, `ApiCacheLogEvent`, and
`ApiAuthLogEvent` values through the existing `ApiLogger` abstraction.

```dart
class AppApiLogger implements ApiLogger {
const AppApiLogger();

@override
void log(ApiLogEvent event) {
// Forward safe structured events to the application's logger.
// Metadata has already passed mandatory redaction.
// Forward it to logging, logger, Crashlytics, Sentry, or another sink.
}
}
```

For callback-style integrations, use `CallbackApiLogger`. The default
`DeveloperApiLogger` uses `ApiLogFormatter`; consumers can also format events
directly with the formatter. ANSI output is off by default.

## Legacy `Server`

`Server.init()` remains as a default-connection compatibility wrapper. New
Expand Down
5 changes: 5 additions & 0 deletions example/lib/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,11 @@ Future<void> main() async {
await Server.init(
baseUrl: 'https://jsonplaceholder.typicode.com',
authStrategy: const CookieStrategy(),
logging: const ApiLoggingConfig(
level: ApiLoggingLevel.basic,
showDuration: true,
showRequestId: false,
),
);

runApp(const MyApp());
Expand Down
14 changes: 14 additions & 0 deletions lib/client/api/api_request_options.dart
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import 'package:dio/dio.dart';

import '../config/api_bridge_config.dart';
import '../logging/api_logger.dart';

/// Shared request controls available on every generated operation.
class ApiRequestOptions {
Expand All @@ -13,6 +14,7 @@ class ApiRequestOptions {
this.idempotencyKey,
this.cancelToken,
this.operationId,
this.log,
this.invalidateCacheTags = const <String>[],
this.invalidateCachePaths = const <String>[],
this.clearActiveSessionCache = false,
Expand All @@ -26,6 +28,7 @@ class ApiRequestOptions {
final String? idempotencyKey;
final CancelToken? cancelToken;
final String? operationId;
final ApiCallLogOptions? log;
final List<String> invalidateCacheTags;
final List<String> invalidateCachePaths;
final bool clearActiveSessionCache;
Expand All @@ -40,6 +43,8 @@ class ApiRequestOptions {
String? idempotencyKey,
CancelToken? cancelToken,
String? operationId,
ApiCallLogOptions? log,
bool clearLog = false,
List<String>? invalidateCacheTags,
List<String>? invalidateCachePaths,
bool? clearActiveSessionCache,
Expand All @@ -53,6 +58,7 @@ class ApiRequestOptions {
idempotencyKey: idempotencyKey ?? this.idempotencyKey,
cancelToken: cancelToken ?? this.cancelToken,
operationId: operationId ?? this.operationId,
log: clearLog ? null : log ?? this.log,
invalidateCacheTags: invalidateCacheTags ?? this.invalidateCacheTags,
invalidateCachePaths: invalidateCachePaths ?? this.invalidateCachePaths,
clearActiveSessionCache:
Expand All @@ -70,6 +76,7 @@ class ApiGetRequestOptions extends ApiRequestOptions {
super.idempotencyKey,
super.cancelToken,
super.operationId,
super.log,
super.invalidateCacheTags,
super.invalidateCachePaths,
super.clearActiveSessionCache,
Expand Down Expand Up @@ -104,6 +111,8 @@ class ApiGetRequestOptions extends ApiRequestOptions {
String? idempotencyKey,
CancelToken? cancelToken,
String? operationId,
ApiCallLogOptions? log,
bool clearLog = false,
List<String>? invalidateCacheTags,
List<String>? invalidateCachePaths,
bool? clearActiveSessionCache,
Expand All @@ -126,6 +135,7 @@ class ApiGetRequestOptions extends ApiRequestOptions {
idempotencyKey: idempotencyKey ?? this.idempotencyKey,
cancelToken: cancelToken ?? this.cancelToken,
operationId: operationId ?? this.operationId,
log: clearLog ? null : log ?? this.log,
invalidateCacheTags: invalidateCacheTags ?? this.invalidateCacheTags,
invalidateCachePaths: invalidateCachePaths ?? this.invalidateCachePaths,
clearActiveSessionCache:
Expand All @@ -150,6 +160,7 @@ class ApiUploadRequestOptions extends ApiRequestOptions {
super.idempotencyKey,
super.cancelToken,
super.operationId,
super.log,
super.invalidateCacheTags,
super.invalidateCachePaths,
super.clearActiveSessionCache,
Expand All @@ -169,6 +180,8 @@ class ApiUploadRequestOptions extends ApiRequestOptions {
String? idempotencyKey,
CancelToken? cancelToken,
String? operationId,
ApiCallLogOptions? log,
bool clearLog = false,
List<String>? invalidateCacheTags,
List<String>? invalidateCachePaths,
bool? clearActiveSessionCache,
Expand All @@ -183,6 +196,7 @@ class ApiUploadRequestOptions extends ApiRequestOptions {
idempotencyKey: idempotencyKey ?? this.idempotencyKey,
cancelToken: cancelToken ?? this.cancelToken,
operationId: operationId ?? this.operationId,
log: clearLog ? null : log ?? this.log,
invalidateCacheTags: invalidateCacheTags ?? this.invalidateCacheTags,
invalidateCachePaths: invalidateCachePaths ?? this.invalidateCachePaths,
clearActiveSessionCache:
Expand Down
24 changes: 14 additions & 10 deletions lib/client/auth/api_auth.dart
Original file line number Diff line number Diff line change
Expand Up @@ -293,22 +293,26 @@ class ApiAuth {
}
_current = next;
_changes.add(next);
if (_logging.enabled) {
final options = _logging.resolve();
final importantFailure = next.status == AuthSessionStatus.expired;
if (!options.enabled ||
options.level == ApiLoggingLevel.errors && !importantFailure) {
return;
}
try {
_logger.log(
ApiLogEvent(
level: next.status == AuthSessionStatus.expired
? ApiLogLevel.warning
: ApiLogLevel.info,
type: ApiLogEventType.auth,
message: 'Authentication state changed',
ApiAuthLogEvent(
level: importantFailure ? ApiLogLevel.warning : ApiLogLevel.info,
timestamp: next.changedAt,
data: <String, Object?>{
options: options,
data: options.redactor.redact(<String, Object?>{
'status': next.status.name,
if (next.sessionId != null) 'sessionId': next.sessionId,
if (next.reason != null) 'reason': next.reason,
},
}) as Map<String, Object?>,
),
);
} catch (_) {
// Diagnostics must never affect authentication state changes.
}
}
}
Loading
Loading