Activity Tracking
Comprehensive activity tracking and audit trails for compliance and debugging
Activity Tracking and Audit Trails
The Vyuh Entity System includes comprehensive activity tracking capabilities that provide audit trails for all entity operations. This guide covers the activity tracking system, implementation patterns, and best practices for maintaining compliance and debugging.
Activity Tracking Overview
The activity tracking system automatically logs:
- Entity creation, updates, and deletions
- User actions and access patterns
- System events and errors
- Custom business events
Core Components
ActivityLog Model
class ActivityLog extends EntityBase {
final String entityType;
final String? entityId;
final String action;
final String userId;
final String userName;
final Map<String, dynamic>? oldValues;
final Map<String, dynamic>? newValues;
final Map<String, dynamic>? metadata;
final String? ipAddress;
final String? userAgent;
final ActivityLogSeverity severity;
final String? errorMessage;
final String? stackTrace;
ActivityLog({
required super.id,
required super.schemaType,
required this.entityType,
this.entityId,
required this.action,
required this.userId,
required this.userName,
this.oldValues,
this.newValues,
this.metadata,
this.ipAddress,
this.userAgent,
this.severity = ActivityLogSeverity.info,
this.errorMessage,
this.stackTrace,
super.createdAt,
super.layout,
super.modifiers,
});
factory ActivityLog.create({
required String entityType,
String? entityId,
required String action,
required String userId,
required String userName,
Map<String, dynamic>? oldValues,
Map<String, dynamic>? newValues,
Map<String, dynamic>? metadata,
}) {
return ActivityLog(
id: const Uuid().v4(),
schemaType: 'activity_logs',
entityType: entityType,
entityId: entityId,
action: action,
userId: userId,
userName: userName,
oldValues: oldValues,
newValues: newValues,
metadata: metadata,
createdAt: DateTime.now(),
);
}
// Convenience constructors
factory ActivityLog.entityCreated({
required String entityType,
required String entityId,
required Map<String, dynamic> entityData,
required String userId,
required String userName,
}) {
return ActivityLog.create(
entityType: entityType,
entityId: entityId,
action: 'created',
userId: userId,
userName: userName,
newValues: entityData,
);
}
factory ActivityLog.entityUpdated({
required String entityType,
required String entityId,
required Map<String, dynamic> oldValues,
required Map<String, dynamic> newValues,
required String userId,
required String userName,
}) {
return ActivityLog.create(
entityType: entityType,
entityId: entityId,
action: 'updated',
userId: userId,
userName: userName,
oldValues: oldValues,
newValues: newValues,
metadata: {
'changedFields': _getChangedFields(oldValues, newValues),
},
);
}
factory ActivityLog.entityDeleted({
required String entityType,
required String entityId,
required Map<String, dynamic> entityData,
required String userId,
required String userName,
}) {
return ActivityLog.create(
entityType: entityType,
entityId: entityId,
action: 'deleted',
userId: userId,
userName: userName,
oldValues: entityData,
);
}
static List<String> _getChangedFields(
Map<String, dynamic> oldValues,
Map<String, dynamic> newValues,
) {
final changedFields = <String>[];
for (final key in newValues.keys) {
if (!oldValues.containsKey(key) || oldValues[key] != newValues[key]) {
changedFields.add(key);
}
}
return changedFields;
}
String get actionDisplay {
switch (action) {
case 'created':
return 'Created';
case 'updated':
return 'Updated';
case 'deleted':
return 'Deleted';
case 'viewed':
return 'Viewed';
case 'exported':
return 'Exported';
case 'imported':
return 'Imported';
case 'approved':
return 'Approved';
case 'rejected':
return 'Rejected';
default:
return action.titleCase;
}
}
String get description {
final entityName = entityType.titleCase;
switch (action) {
case 'created':
return '$userName created $entityName';
case 'updated':
final fields = metadata?['changedFields'] as List<dynamic>?;
if (fields != null && fields.isNotEmpty) {
return '$userName updated ${fields.join(", ")} in $entityName';
}
return '$userName updated $entityName';
case 'deleted':
return '$userName deleted $entityName';
default:
return '$userName $action $entityName';
}
}
factory ActivityLog.fromJson(Map<String, dynamic> json) =>
_$ActivityLogFromJson(json);
@override
Map<String, dynamic> toJson() => _$ActivityLogToJson(this);
}
enum ActivityLogSeverity {
debug,
info,
warning,
error,
critical,
}ActivityApi
class ActivityApi extends EntityApi<ActivityLog> {
ActivityApi({required super.client});
@override
ActivityLog fromJson(Map<String, dynamic> json) =>
ActivityLog.fromJson(json);
Future<List<ActivityLog>> getEntityHistory(
String entityType,
String entityId, {
int? limit,
DateTime? startDate,
DateTime? endDate,
}) async {
final response = await client.get(
'/api/v1/activity-logs',
queryParameters: {
'entityType': entityType,
'entityId': entityId,
if (limit != null) 'limit': limit.toString(),
if (startDate != null) 'startDate': startDate.toIso8601String(),
if (endDate != null) 'endDate': endDate.toIso8601String(),
'sortBy': 'createdAt',
'sortOrder': 'desc',
},
);
final List<dynamic> data = response.data['data'];
return data.map((json) => fromJson(json)).toList();
}
Future<List<ActivityLog>> getUserActivity(
String userId, {
int? limit,
DateTime? startDate,
DateTime? endDate,
}) async {
final response = await client.get(
'/api/v1/activity-logs',
queryParameters: {
'userId': userId,
if (limit != null) 'limit': limit.toString(),
if (startDate != null) 'startDate': startDate.toIso8601String(),
if (endDate != null) 'endDate': endDate.toIso8601String(),
'sortBy': 'createdAt',
'sortOrder': 'desc',
},
);
final List<dynamic> data = response.data['data'];
return data.map((json) => fromJson(json)).toList();
}
Future<Map<String, dynamic>> getActivityStats({
DateTime? startDate,
DateTime? endDate,
}) async {
final response = await client.get(
'/api/v1/activity-logs/stats',
queryParameters: {
if (startDate != null) 'startDate': startDate.toIso8601String(),
if (endDate != null) 'endDate': endDate.toIso8601String(),
},
);
return response.data;
}
Future<void> logActivity(ActivityLog activity) async {
await createNew(activity);
}
Future<void> logBatch(List<ActivityLog> activities) async {
await client.post(
'/api/v1/activity-logs/batch',
data: {
'activities': activities.map((a) => a.toJson()).toList(),
},
);
}
}Activity Tracking Service
ActivityTrackingService
class ActivityTrackingService {
final ActivityApi api;
final AuthService authService;
final Queue<ActivityLog> _queue = Queue();
Timer? _flushTimer;
bool _isProcessing = false;
ActivityTrackingService({
required this.api,
required this.authService,
}) {
// Start periodic flush
_flushTimer = Timer.periodic(
const Duration(seconds: 5),
(_) => _flushQueue(),
);
}
Future<void> trackEntityCreated<T extends EntityBase>(T entity) async {
final user = await authService.getCurrentUser();
if (user == null) return;
final activity = ActivityLog.entityCreated(
entityType: T.toString(),
entityId: entity.id,
entityData: entity.toJson(),
userId: user.id,
userName: user.name,
);
_enqueue(activity);
}
Future<void> trackEntityUpdated<T extends EntityBase>(
T oldEntity,
T newEntity,
) async {
final user = await authService.getCurrentUser();
if (user == null) return;
final activity = ActivityLog.entityUpdated(
entityType: T.toString(),
entityId: newEntity.id,
oldValues: oldEntity.toJson(),
newValues: newEntity.toJson(),
userId: user.id,
userName: user.name,
);
_enqueue(activity);
}
Future<void> trackEntityDeleted<T extends EntityBase>(T entity) async {
final user = await authService.getCurrentUser();
if (user == null) return;
final activity = ActivityLog.entityDeleted(
entityType: T.toString(),
entityId: entity.id,
entityData: entity.toJson(),
userId: user.id,
userName: user.name,
);
_enqueue(activity);
}
Future<void> trackCustomAction({
required String entityType,
String? entityId,
required String action,
Map<String, dynamic>? metadata,
}) async {
final user = await authService.getCurrentUser();
if (user == null) return;
final activity = ActivityLog.create(
entityType: entityType,
entityId: entityId,
action: action,
userId: user.id,
userName: user.name,
metadata: metadata,
);
_enqueue(activity);
}
Future<void> trackError({
required String entityType,
String? entityId,
required String action,
required String error,
String? stackTrace,
}) async {
final user = await authService.getCurrentUser();
final activity = ActivityLog(
id: const Uuid().v4(),
schemaType: 'activity_logs',
entityType: entityType,
entityId: entityId,
action: action,
userId: user?.id ?? 'system',
userName: user?.name ?? 'System',
severity: ActivityLogSeverity.error,
errorMessage: error,
stackTrace: stackTrace,
createdAt: DateTime.now(),
);
// Errors should be logged immediately
try {
await api.logActivity(activity);
} catch (e) {
// Log to console if API fails
debugPrint('Failed to log error activity: $e');
_enqueue(activity); // Try again later
}
}
void _enqueue(ActivityLog activity) {
_queue.add(activity);
// Flush immediately if queue is getting large
if (_queue.length >= 50) {
_flushQueue();
}
}
Future<void> _flushQueue() async {
if (_isProcessing || _queue.isEmpty) return;
_isProcessing = true;
final batch = <ActivityLog>[];
// Take up to 100 items from queue
while (_queue.isNotEmpty && batch.length < 100) {
batch.add(_queue.removeFirst());
}
try {
await api.logBatch(batch);
} catch (e) {
// Put items back in queue on failure
for (final activity in batch.reversed) {
_queue.addFirst(activity);
}
debugPrint('Failed to flush activity queue: $e');
} finally {
_isProcessing = false;
}
}
void dispose() {
_flushTimer?.cancel();
// Final flush
_flushQueue();
}
}Automatic Activity Tracking
class TrackedEntityApi<T extends EntityBase> extends EntityApi<T> {
final ActivityTrackingService trackingService;
TrackedEntityApi({
required super.client,
required this.trackingService,
});
@override
Future<T> performCreate(T entity) async {
final created = await super.performCreate(entity);
await trackingService.trackEntityCreated(created);
return created;
}
@override
Future<T> performUpdate(String id, T entity) async {
// Get old entity for comparison
final oldEntity = await performGetById(id);
if (oldEntity == null) {
throw NotFoundException('Entity not found: $id');
}
final updated = await super.performUpdate(id, entity);
await trackingService.trackEntityUpdated(oldEntity, updated);
return updated;
}
@override
Future<void> performDelete(String id) async {
// Get entity before deletion
final entity = await performGetById(id);
if (entity != null) {
await trackingService.trackEntityDeleted(entity);
}
await super.performDelete(id);
}
}Activity UI Components
Activity Timeline
class ActivityTimeline extends StatelessWidget {
final String? entityType;
final String? entityId;
final String? userId;
const ActivityTimeline({
this.entityType,
this.entityId,
this.userId,
});
@override
Widget build(BuildContext context) {
return FutureBuilder<List<ActivityLog>>(
future: _loadActivities(context),
builder: (context, snapshot) {
if (!snapshot.hasData) {
return const Center(child: CircularProgressIndicator());
}
final activities = snapshot.data!;
if (activities.isEmpty) {
return const EmptyState(
icon: Icons.history,
title: 'No Activity',
subtitle: 'No activity has been recorded yet',
);
}
// Group activities by date
final groupedActivities = groupBy(
activities,
(activity) => DateFormat('yyyy-MM-dd').format(activity.createdAt!),
);
return ListView.builder(
itemCount: groupedActivities.length,
itemBuilder: (context, index) {
final date = groupedActivities.keys.elementAt(index);
final dayActivities = groupedActivities[date]!;
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Date header
Padding(
padding: const EdgeInsets.all(16),
child: Text(
_formatDateHeader(date),
style: Theme.of(context).textTheme.titleMedium?.copyWith(
color: Theme.of(context).textTheme.bodySmall?.color,
),
),
),
// Activities for this date
...dayActivities.map((activity) => ActivityTimelineItem(
activity: activity,
onTap: () => _showActivityDetails(context, activity),
)),
],
);
},
);
},
);
}
Future<List<ActivityLog>> _loadActivities(BuildContext context) async {
final api = context.read<ActivityApi>();
if (entityId != null && entityType != null) {
return api.getEntityHistory(entityType!, entityId!);
} else if (userId != null) {
return api.getUserActivity(userId!);
} else {
return api.list(limit: 100);
}
}
String _formatDateHeader(String date) {
final parsedDate = DateTime.parse(date);
final now = DateTime.now();
final today = DateTime(now.year, now.month, now.day);
final yesterday = today.subtract(const Duration(days: 1));
if (parsedDate.isAtSameMomentAs(today)) {
return 'Today';
} else if (parsedDate.isAtSameMomentAs(yesterday)) {
return 'Yesterday';
} else {
return DateFormat('MMMM d, y').format(parsedDate);
}
}
void _showActivityDetails(BuildContext context, ActivityLog activity) {
showDialog(
context: context,
builder: (_) => ActivityDetailsDialog(activity: activity),
);
}
}
class ActivityTimelineItem extends StatelessWidget {
final ActivityLog activity;
final VoidCallback? onTap;
const ActivityTimelineItem({
required this.activity,
this.onTap,
});
@override
Widget build(BuildContext context) {
return InkWell(
onTap: onTap,
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Time
SizedBox(
width: 60,
child: Text(
DateFormat('HH:mm').format(activity.createdAt!),
style: Theme.of(context).textTheme.bodySmall,
),
),
// Icon
Container(
width: 32,
height: 32,
decoration: BoxDecoration(
color: _getColorForAction(activity.action).withOpacity(0.1),
shape: BoxShape.circle,
),
child: Icon(
_getIconForAction(activity.action),
size: 16,
color: _getColorForAction(activity.action),
),
),
const SizedBox(width: 12),
// Content
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
activity.description,
style: Theme.of(context).textTheme.bodyMedium,
),
if (activity.metadata != null &&
activity.metadata!.isNotEmpty)
Padding(
padding: const EdgeInsets.only(top: 4),
child: Text(
_getMetadataDisplay(activity),
style: Theme.of(context).textTheme.bodySmall,
),
),
],
),
),
],
),
),
);
}
IconData _getIconForAction(String action) {
switch (action) {
case 'created':
return Icons.add_circle_outline;
case 'updated':
return Icons.edit_outlined;
case 'deleted':
return Icons.delete_outline;
case 'viewed':
return Icons.visibility_outlined;
case 'exported':
return Icons.download_outlined;
case 'imported':
return Icons.upload_outlined;
default:
return Icons.circle_outlined;
}
}
Color _getColorForAction(String action) {
switch (action) {
case 'created':
return Colors.green;
case 'updated':
return Colors.blue;
case 'deleted':
return Colors.red;
default:
return Colors.grey;
}
}
String _getMetadataDisplay(ActivityLog activity) {
final metadata = activity.metadata!;
final parts = <String>[];
if (metadata['changedFields'] != null) {
final fields = (metadata['changedFields'] as List).join(', ');
parts.add('Changed: $fields');
}
if (metadata['reason'] != null) {
parts.add('Reason: ${metadata['reason']}');
}
return parts.join(' • ');
}
}Activity Details Dialog
class ActivityDetailsDialog extends StatelessWidget {
final ActivityLog activity;
const ActivityDetailsDialog({required this.activity});
@override
Widget build(BuildContext context) {
return Dialog(
child: Container(
width: 600,
padding: const EdgeInsets.all(24),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Header
Row(
children: [
Icon(
_getIconForAction(activity.action),
color: _getColorForAction(activity.action),
),
const SizedBox(width: 8),
Text(
activity.actionDisplay,
style: Theme.of(context).textTheme.titleLarge,
),
const Spacer(),
IconButton(
icon: const Icon(Icons.close),
onPressed: () => Navigator.of(context).pop(),
),
],
),
const Divider(height: 32),
// Details
_buildDetailRow('User', activity.userName),
_buildDetailRow('Entity Type', activity.entityType.titleCase),
if (activity.entityId != null)
_buildDetailRow('Entity ID', activity.entityId!),
_buildDetailRow(
'Timestamp',
DateFormat('MMM d, y HH:mm:ss').format(activity.createdAt!),
),
// Changed fields
if (activity.oldValues != null && activity.newValues != null) ...[
const SizedBox(height: 16),
Text(
'Changes',
style: Theme.of(context).textTheme.titleMedium,
),
const SizedBox(height: 8),
..._buildChanges(context),
],
// Metadata
if (activity.metadata != null && activity.metadata!.isNotEmpty) ...[
const SizedBox(height: 16),
Text(
'Additional Information',
style: Theme.of(context).textTheme.titleMedium,
),
const SizedBox(height: 8),
Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surface,
borderRadius: BorderRadius.circular(8),
),
child: Text(
JsonEncoder.withIndent(' ').convert(activity.metadata),
style: Theme.of(context).textTheme.bodySmall?.copyWith(
fontFamily: 'monospace',
),
),
),
],
// Actions
const SizedBox(height: 24),
Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
if (activity.entityId != null)
TextButton(
onPressed: () {
Navigator.of(context).pop();
context.go('/${activity.entityType}/${activity.entityId}');
},
child: const Text('View Entity'),
),
const SizedBox(width: 8),
ElevatedButton(
onPressed: () => Navigator.of(context).pop(),
child: const Text('Close'),
),
],
),
],
),
),
);
}
Widget _buildDetailRow(String label, String value) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 4),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SizedBox(
width: 120,
child: Text(
label,
style: const TextStyle(fontWeight: FontWeight.bold),
),
),
Expanded(child: Text(value)),
],
),
);
}
List<Widget> _buildChanges(BuildContext context) {
final changes = <Widget>[];
final oldValues = activity.oldValues!;
final newValues = activity.newValues!;
for (final key in newValues.keys) {
if (oldValues.containsKey(key) && oldValues[key] != newValues[key]) {
changes.add(
Container(
margin: const EdgeInsets.only(bottom: 8),
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
border: Border.all(color: Theme.of(context).dividerColor),
borderRadius: BorderRadius.circular(8),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
key.titleCase,
style: const TextStyle(fontWeight: FontWeight.bold),
),
const SizedBox(height: 4),
Row(
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'Old Value',
style: TextStyle(fontSize: 12),
),
Text(
oldValues[key]?.toString() ?? 'null',
style: const TextStyle(
decoration: TextDecoration.lineThrough,
color: Colors.red,
),
),
],
),
),
const Icon(Icons.arrow_forward, size: 16),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'New Value',
style: TextStyle(fontSize: 12),
),
Text(
newValues[key]?.toString() ?? 'null',
style: const TextStyle(color: Colors.green),
),
],
),
),
],
),
],
),
),
);
}
}
return changes;
}
}Activity Analytics
Activity Dashboard Widget
class ActivityDashboardWidget extends StatelessWidget {
final DateTimeRange dateRange;
const ActivityDashboardWidget({required this.dateRange});
@override
Widget build(BuildContext context) {
return FutureBuilder<Map<String, dynamic>>(
future: _loadStats(context),
builder: (context, snapshot) {
if (!snapshot.hasData) {
return const LoadingIndicator();
}
final stats = snapshot.data!;
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Summary cards
Row(
children: [
Expanded(
child: _StatCard(
title: 'Total Activities',
value: stats['total'].toString(),
icon: Icons.timeline,
color: Colors.blue,
),
),
const SizedBox(width: 16),
Expanded(
child: _StatCard(
title: 'Active Users',
value: stats['activeUsers'].toString(),
icon: Icons.people,
color: Colors.green,
),
),
const SizedBox(width: 16),
Expanded(
child: _StatCard(
title: 'Entities Modified',
value: stats['entitiesModified'].toString(),
icon: Icons.edit,
color: Colors.orange,
),
),
],
),
const SizedBox(height: 24),
// Activity by type chart
Card(
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Activity by Type',
style: Theme.of(context).textTheme.titleMedium,
),
const SizedBox(height: 16),
SizedBox(
height: 200,
child: ActivityTypeChart(
data: stats['byType'] as Map<String, int>,
),
),
],
),
),
),
const SizedBox(height: 16),
// Recent activities
Card(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.all(16),
child: Row(
children: [
Text(
'Recent Activities',
style: Theme.of(context).textTheme.titleMedium,
),
const Spacer(),
TextButton(
onPressed: () => context.go('/activity-logs'),
child: const Text('View All'),
),
],
),
),
const Divider(height: 1),
LimitedBox(
maxHeight: 300,
child: ActivityTimeline(),
),
],
),
),
],
);
},
);
}
Future<Map<String, dynamic>> _loadStats(BuildContext context) async {
final api = context.read<ActivityApi>();
return api.getActivityStats(
startDate: dateRange.start,
endDate: dateRange.end,
);
}
}Export and Compliance
Activity Export
class ActivityExporter {
final ActivityApi api;
const ActivityExporter({required this.api});
Future<String> exportToCsv({
String? entityType,
String? entityId,
String? userId,
DateTime? startDate,
DateTime? endDate,
}) async {
// Load activities
final activities = await api.list(
filters: {
if (entityType != null) 'entityType': entityType,
if (entityId != null) 'entityId': entityId,
if (userId != null) 'userId': userId,
},
sortBy: 'createdAt',
sortOrder: 'desc',
);
// Build CSV
final csv = StringBuffer();
// Header
csv.writeln(
'Timestamp,User,Action,Entity Type,Entity ID,Description,Changes'
);
// Data
for (final activity in activities) {
final timestamp = DateFormat('yyyy-MM-dd HH:mm:ss')
.format(activity.createdAt!);
final changes = _getChangeSummary(activity);
csv.writeln(
'$timestamp,"${activity.userName}","${activity.action}",'
'"${activity.entityType}","${activity.entityId ?? ""}",'
'"${activity.description}","$changes"'
);
}
return csv.toString();
}
Future<Uint8List> exportToPdf({
String? entityType,
String? entityId,
DateTime? startDate,
DateTime? endDate,
}) async {
// Implementation depends on PDF library
throw UnimplementedError();
}
String _getChangeSummary(ActivityLog activity) {
if (activity.oldValues == null || activity.newValues == null) {
return '';
}
final changes = <String>[];
final changedFields = activity.metadata?['changedFields'] as List?;
if (changedFields != null) {
for (final field in changedFields) {
final oldValue = activity.oldValues![field];
final newValue = activity.newValues![field];
changes.add('$field: $oldValue → $newValue');
}
}
return changes.join('; ');
}
}Compliance Reports
class ComplianceReportGenerator {
final ActivityApi api;
const ComplianceReportGenerator({required this.api});
Future<ComplianceReport> generateReport({
required DateTime startDate,
required DateTime endDate,
List<String>? entityTypes,
List<String>? actions,
}) async {
final activities = await api.list(
filters: {
'startDate': startDate.toIso8601String(),
'endDate': endDate.toIso8601String(),
if (entityTypes != null) 'entityTypes': entityTypes,
if (actions != null) 'actions': actions,
},
);
return ComplianceReport(
period: DateTimeRange(start: startDate, end: endDate),
totalActivities: activities.length,
uniqueUsers: activities.map((a) => a.userId).toSet().length,
activitiesByType: _groupByType(activities),
activitiesByUser: _groupByUser(activities),
sensitiveOperations: _findSensitiveOperations(activities),
anomalies: _detectAnomalies(activities),
);
}
Map<String, int> _groupByType(List<ActivityLog> activities) {
final grouped = <String, int>{};
for (final activity in activities) {
final key = '${activity.entityType}.${activity.action}';
grouped[key] = (grouped[key] ?? 0) + 1;
}
return grouped;
}
Map<String, int> _groupByUser(List<ActivityLog> activities) {
final grouped = <String, int>{};
for (final activity in activities) {
grouped[activity.userName] = (grouped[activity.userName] ?? 0) + 1;
}
return grouped;
}
List<ActivityLog> _findSensitiveOperations(List<ActivityLog> activities) {
return activities.where((activity) {
// Define sensitive operations
return activity.action == 'deleted' ||
activity.entityType == 'users' ||
activity.entityType == 'permissions' ||
activity.severity == ActivityLogSeverity.critical;
}).toList();
}
List<ComplianceAnomaly> _detectAnomalies(List<ActivityLog> activities) {
final anomalies = <ComplianceAnomaly>[];
// Check for unusual activity patterns
final userActivityCount = <String, int>{};
for (final activity in activities) {
userActivityCount[activity.userId] =
(userActivityCount[activity.userId] ?? 0) + 1;
}
// Flag users with excessive activity
final avgActivity = activities.length / userActivityCount.length;
for (final entry in userActivityCount.entries) {
if (entry.value > avgActivity * 3) {
anomalies.add(ComplianceAnomaly(
type: 'excessive_activity',
userId: entry.key,
description: 'User has ${entry.value} activities, '
'3x more than average',
severity: AnomalySeverity.medium,
));
}
}
// Check for after-hours activity
for (final activity in activities) {
final hour = activity.createdAt!.hour;
if (hour < 6 || hour > 22) {
anomalies.add(ComplianceAnomaly(
type: 'after_hours',
userId: activity.userId,
description: 'Activity at ${activity.createdAt}',
severity: AnomalySeverity.low,
activityId: activity.id,
));
}
}
return anomalies;
}
}Configuration
Activity Tracking Settings
class ActivityTrackingSettings {
final bool enabled;
final bool trackReads;
final bool trackFieldChanges;
final List<String> excludedEntityTypes;
final List<String> excludedFields;
final Duration retentionPeriod;
final int batchSize;
final Duration flushInterval;
const ActivityTrackingSettings({
this.enabled = true,
this.trackReads = false,
this.trackFieldChanges = true,
this.excludedEntityTypes = const [],
this.excludedFields = const ['password', 'token', 'secret'],
this.retentionPeriod = const Duration(days: 365),
this.batchSize = 50,
this.flushInterval = const Duration(seconds: 5),
});
}
// Usage
final settings = ActivityTrackingSettings(
trackReads: true, // Enable read tracking
excludedEntityTypes: ['temp_data'], // Don't track temporary entities
excludedFields: ['password', 'apiKey', 'secret'], // Sensitive fields
retentionPeriod: const Duration(days: 730), // 2 years
);Best Practices
- Performance - Use batch logging for better performance
- Privacy - Exclude sensitive fields from logs
- Retention - Define clear retention policies
- Compliance - Regular compliance reports
- Security - Protect activity logs from tampering
- Analysis - Use activity data for insights
- Real-time - Consider real-time monitoring for critical actions
Next: Help System - Context-sensitive help integration