From 1912cfa47376faca1f64f4dcc3af6a5150ffc8d0 Mon Sep 17 00:00:00 2001 From: Thiago Moreira Date: Wed, 23 Jul 2025 23:28:59 -0300 Subject: [PATCH] feat: add HTTP tracking and Clarity adapter --- .gitignore | 4 +- CHANGELOG.md | 430 ++--- README.md | 1141 ++++++++---- example/analysis_options.yaml | 207 +++ example/lib/adapters_example.dart | 532 +++--- example/lib/analytics_example.dart | 213 ++- example/lib/google_logging_example.dart | 549 +++--- example/lib/http_tracking_example.dart | 1553 ++++++++--------- example/lib/main.dart | 358 ++-- example/lib/view_tracking_example.dart | 874 +++++----- lib/src/analytics/adapters/adapters.dart | 1 + .../adapters/engine_clarity_adapter.dart | 82 + .../engine_faro_analytics_adapter.dart | 54 +- .../engine_firebase_analytics_adapter.dart | 19 +- ...gine_google_logging_analytics_adapter.dart | 26 +- .../engine_splunk_analytics_adapter.dart | 31 +- .../adapters/i_engine_analytics_adapter.dart | 5 +- lib/src/analytics/engine_analytics.dart | 100 +- .../adapters/engine_crashlytics_adapter.dart | 28 +- .../engine_faro_bug_tracking_adapter.dart | 44 +- ...e_google_logging_bug_tracking_adapter.dart | 27 +- .../i_engine_bug_tracking_adapter.dart | 4 +- lib/src/bug_tracking/engine_bug_tracking.dart | 95 +- lib/src/config/config.dart | 2 + lib/src/config/engine_clarity_config.dart | 16 +- lib/src/config/engine_crashlytics_config.dart | 6 +- lib/src/config/engine_faro_config.dart | 11 +- .../engine_firebase_analytics_config.dart | 6 +- .../config/engine_google_logging_config.dart | 9 +- .../config/engine_http_tracking_config.dart | 52 + lib/src/config/engine_splunk_config.dart | 9 +- lib/src/config/i_engine_config.dart | 5 + lib/src/engine_tracking_initialize.dart | 18 + lib/src/http/engine_http_client.dart | 162 ++ lib/src/http/engine_http_client_request.dart | 212 +++ lib/src/http/engine_http_override.dart | 65 + lib/src/http/engine_http_tracking.dart | 144 ++ lib/src/logging/engine_log.dart | 21 +- lib/src/models/engine_analytics_model.dart | 26 +- lib/src/models/engine_bug_tracking_model.dart | 24 +- .../observers/engine_navigator_observer.dart | 12 +- lib/src/src.dart | 1 + lib/src/widgets/engine_widget.dart | 71 +- pubspec.yaml | 13 +- test/analytics/engine_analytics_test.dart | 80 +- .../engine_bug_tracking_test.dart | 74 +- .../engine_crashlytics_config_test.dart | 27 +- test/config/engine_faro_config_test.dart | 8 +- ...engine_firebase_analytics_config_test.dart | 33 +- test/config/engine_splunk_config_test.dart | 41 +- test/engine_tracking_initialize_test.dart | 54 +- test/helpers/test_configs.dart | 4 +- test/models/engine_analytics_model_test.dart | 24 +- .../engine_bug_tracking_model_test.dart | 8 +- test/test_coverage.dart | 32 +- test/utils/engine_http_override_test.dart | 98 ++ test/utils/engine_http_tracking_test.dart | 186 ++ test/widgets/engine_widget_test.dart | 191 ++ 58 files changed, 4977 insertions(+), 3145 deletions(-) create mode 100644 lib/src/analytics/adapters/engine_clarity_adapter.dart create mode 100644 lib/src/config/engine_http_tracking_config.dart create mode 100644 lib/src/config/i_engine_config.dart create mode 100644 lib/src/http/engine_http_client.dart create mode 100644 lib/src/http/engine_http_client_request.dart create mode 100644 lib/src/http/engine_http_override.dart create mode 100644 lib/src/http/engine_http_tracking.dart create mode 100644 test/utils/engine_http_override_test.dart create mode 100644 test/utils/engine_http_tracking_test.dart create mode 100644 test/widgets/engine_widget_test.dart diff --git a/.gitignore b/.gitignore index 874148d..e82cd95 100644 --- a/.gitignore +++ b/.gitignore @@ -98,4 +98,6 @@ dev-debug.log # Task files # tasks.json # tasks/ -docs/ \ No newline at end of file +docs/ +.kiro/ +.cursor/ diff --git a/CHANGELOG.md b/CHANGELOG.md index 44eb589..504a2d4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,366 +1,148 @@ # Changelog -All notable changes to this project will be documented in this file. +All notable changes to the Engine Tracking library will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). -## [1.5.0] +## [1.5.0] - 2025-01-23 ### Added -- **EngineTrackingInitialize**: Central initializer for analytics and bug tracking services -- **Nullable Parameters**: Optional initialization of analytics or bug tracking individually -- **Dartdoc Documentation**: Complete public API documentation for all methods and parameters - -### Enhanced -- **Flexible Initialization**: Initialize both services, only analytics, or only bug tracking -- **Parallel Execution**: Optimized Future.wait for maximum performance -- **Test Coverage**: 17 new tests for centralized initialization scenarios - -## [1.4.1] - -### Enhanced -- **EngineClarityConfig**: Enhanced configuration options for better Clarity integration -- **EngineLogLevelType**: Added `none` and `verbose` log levels for improved filtering -- **EngineLog**: Enhanced logging functionality with better level management -- **EngineWidget**: Refined widget integration for improved tracking performance - -### Fixed -- **Dependency Optimization**: Moved `http` to dev_dependencies, removed unused `firebase_core` -- **Test Coverage**: Updated enum tests to reflect new log level values - -## [1.4.0] - 2025-01-24 - -### Added -- **šŸŽ„ Microsoft Clarity Integration**: Complete integration with official Clarity Flutter SDK for behavioral analytics -- **EngineClarityConfig**: Configuration class for Microsoft Clarity with Project ID, User ID, and LogLevel support -- **Masking Widgets**: `EngineMaskWidget` and `EngineUnmaskWidget` for protecting sensitive content -- **Example App**: Complete example demonstrating Clarity integration with masking examples - -### Enhanced -#### Microsoft Clarity Features -- **Session Recordings**: Automatic capture of user sessions for replay -- **Heatmaps**: Visual representation of user interactions -- **User Insights**: Automatic detection of rage taps, dead taps, excessive scrolling -- **Auto-tracking**: Automatic capture of navigation and user interactions -- **Zero Configuration Events**: No manual event logging needed - Clarity captures automatically - -#### Architecture Updates -- **EngineAnalyticsModel**: Added `clarityConfig` property for Clarity configuration -- **EngineAnalytics**: Added `isClarityInitialized` -- **Widget Exports**: Added Clarity masking widgets to widget exports -- **Adapter Pattern**: Adapted Clarity's unique widget-based initialization to Engine Tracking architecture +- **EngineClarityAdapter**: New dedicated adapter for Microsoft Clarity analytics +- Enhanced Microsoft Clarity integration with proper adapter pattern +- Clarity-specific status verification methods in EngineAnalytics +- Automatic session ID synchronization with Clarity sessions + +### Changed +- Improved documentation structure and readability +- Simplified architecture diagrams for better understanding +- Enhanced code examples with real-world scenarios +- Updated README.md with professional tone and clearer sections +- Microsoft Clarity now uses dedicated adapter instead of widget-only integration ### Dependencies -- **clarity_flutter: ^1.0.0**: Official Microsoft Clarity Flutter SDK - -### Technical Details -- **Unique Implementation**: Clarity requires wrapping the app with ClarityWidget instead of static methods -- **LogLevel Support**: Automatic production optimization (LogLevel.None in release builds) -- **User ID Validation**: Base-36 format validation for Clarity user IDs -- **Session Recording**: ~30 minutes for real-time viewing, ~2 hours for complete processing - -## [1.3.0] - 2025-01-23 - -### Added -- **šŸ†” Session ID AutomĆ”tico**: Sistema de correlação de logs e analytics atravĆ©s de UUID v4 Ćŗnico por sessĆ£o -- **EngineSession**: Nova classe singleton para gerenciamento de Session ID -- **Auto-inject**: Session ID incluĆ­do automaticamente em todos os eventos e logs -- **Validação RFC 4122**: Formato UUID v4 compatĆ­vel com qualquer sistema -- **Testes Completos**: 9 testes unitĆ”rios para Session ID com validação de conformidade - -### Enhanced -#### Sistema de Session ID -- **Zero Configuração**: Session ID gerado automaticamente na primeira chamada -- **Correlação Universal**: UUID v4 incluĆ­do em Firebase Analytics, Google Cloud Logging, Crashlytics e Faro -- **Singleton Pattern**: Mesma instĆ¢ncia durante toda a vida do app -- **Formato PadrĆ£o**: `xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx` (RFC 4122 UUID v4) -- **TestĆ”vel**: MĆ©todo `resetForTesting()` para cenĆ”rios de teste - -#### Integração AutomĆ”tica -- **Firebase Analytics**: Session ID em todos os eventos automaticamente -- **Google Cloud Logging**: Correlation ID para agrupamento de logs -- **EngineLog**: Session ID incluĆ­do em todos os nĆ­veis de log -- **MĆ©todo Enrich**: `enrichWithSessionId()` para auto-inject em dados - -#### Arquitetura Atualizada -- **Diagrama Mermaid**: Novo diagrama mostrando fluxo do Session ID -- **Documentação Completa**: Seção dedicada com exemplos prĆ”ticos -- **Casos de Uso**: Exemplos de correlação em painĆ©is de analytics - -### Quality Improvements -- **96 Testes Passando**: Atualização de 87 para 96 testes (100% de sucesso) -- **UUID v4 Conformance**: Validação completa do formato RFC 4122 -- **Unicidade Testada**: Verificação de 1000/1000 UUIDs Ćŗnicos gerados -- **Performance**: Geração eficiente de UUID sem dependĆŖncias externas +- Updated `firebase_crashlytics` from ^4.3.7 to ^4.3.10 - Enhanced crash reporting stability +- Updated `firebase_analytics` from ^11.5.0 to ^11.6.0 - Latest Firebase Analytics features and improvements +- Updated `faro` from ^0.3.6 to ^0.4.1 - Improved Grafana Faro observability capabilities +- Updated `clarity_flutter` from ^1.0.0 to ^1.2.0 - Enhanced Microsoft Clarity integration +- Updated `mockito` from ^5.4.4 to ^5.5.0 (dev dependency) - Better testing framework support +- Updated `build_runner` from ^2.4.9 to ^2.6.0 (dev dependency) - Improved code generation performance ### Documentation -- **README Atualizado**: Seção completa sobre Session ID -- **Exemplos PrĆ”ticos**: Como usar Session ID para correlação de logs -- **Queries de Exemplo**: Como consultar logs por session_id nos painĆ©is -- **Melhores PrĆ”ticas**: Uso do Session ID para anĆ”lise de jornada do usuĆ”rio - -## [1.2.1] - 2025-01-15 +- Added comprehensive changelog +- Restructured README.md for better developer experience +- Simplified Mermaid diagrams with English labels +- Improved installation and setup instructions +- Enhanced code examples and usage patterns +- Updated architecture diagrams to include Clarity adapter -### Enhanced -#### šŸ“‹ Documentação de Arquitetura -- **Diagramas Mermaid**: Adicionados 4 diagramas completos da arquitetura no README: - - **Widgets Stateless/Stateful**: Mostra execução de mĆ©todos e lifecycle tracking - - **Sistema de Logging (EngineLog)**: Fluxo detalhado com condicionais de Analytics e Bug Tracking - - **Sistema de Analytics**: Arquitetura de adapters e integração com dashboards - - **Sistema de Bug Tracking**: Fluxo de captura de erros e crash reporting +## [1.4.0] - 2024-12-15 -#### šŸ”§ Melhorias no Diagrama EngineLog -- **Condicionais Claras**: Representação visual das condiƧƵes `EngineAnalytics.isEnabled && includeInAnalytics` -- **Fluxo de Erro**: Mostra que logs de level `error` e `fatal` geram crash reporting adicional -- **Nomenclatura Melhorada**: ParĆ¢metro `includeInAnalytics` mais descritivo que `hasAnalytics` -- **Estilização Visual**: Condicionais destacadas com cores para melhor legibilidade - -#### šŸŽØ Recursos Visuais -- **Cores Organizadas**: Paleta de cores consistente por tipo de componente -- **Formas Diferenciadas**: Losangos para condicionais, retĆ¢ngulos para componentes -- **Legenda IncluĆ­da**: Facilita compreensĆ£o da arquitetura -- **Fluxo HierĆ”rquico**: Visualização clara do fluxo de dados de cima para baixo +### Added +- Session ID automatic correlation system with UUID v4 generation +- Centralized initialization with `EngineTrackingInitialize` class +- Enhanced widget tracking with `EngineStatelessWidget` and `EngineStatefulWidget` +- Comprehensive logging system with `EngineLog` and multiple levels +- HTTP request tracking capabilities with automatic monitoring +- Navigation observer with `EngineNavigationObserver` for automatic screen tracking +- Custom widgets with built-in tracking: `EngineWidget`, `EngineMaskWidget` + +### Changed +- Improved session management with automatic correlation across all services +- Enhanced error handling with automatic Flutter error capture +- Better adapter pattern implementation for service integrations +- Optimized performance with conditional service initialization -### Documentation -- **Arquitetura Completa**: Seção dedicada mostrando como toda a solução funciona integrada -- **Fluxos Condicionais**: Demonstra quando Analytics e Bug Tracking sĆ£o ativados -- **Representação Fiel**: Diagramas 100% alinhados com a implementação real do código +### Features +- Multi-platform analytics support (Firebase, Faro, Splunk, Google Cloud Logging) +- Advanced bug tracking with Firebase Crashlytics integration +- Type-safe implementation with full Dart null safety +- Flexible service configuration (enable/disable individual services) -## [1.1.1] - 2025-01-23 +## [1.3.0] - 2024-11-20 ### Added -- **🌐 HTTP Tracking Example**: Novo exemplo completo demonstrando tracking de requisiƧƵes HTTPS - -### Enhanced -#### Exemplo HTTP Tracking -- **PokĆ©API Integration**: Demonstração de requisiƧƵes GET para dados de pokĆ©mons -- **JSONPlaceholder Integration**: Exemplo completo com GET e POST para posts e usuĆ”rios -- **MĆ©tricas Detalhadas**: Tracking automĆ”tico de: - - Tempo de resposta em milissegundos - - Códigos de status HTTP - - Tamanho das respostas em bytes - - Sucesso/falha das requisiƧƵes - - Timestamps completos -- **Tratamento de Erros**: Sistema robusto de captura e logging de erros HTTP -- **Interface Responsiva**: Design adaptativo com scroll automĆ”tico - -#### Funcionalidades das APIs -- **Pokemon List Page**: Lista interativa de pokĆ©mons com detalhes em modal -- **Posts List Page**: Visualização e criação de posts com tracking completo -- **Users List Page**: Lista detalhada de usuĆ”rios com informaƧƵes completas +- Google Cloud Logging integration for analytics and bug tracking +- Microsoft Clarity support for session recordings and heatmaps +- Splunk integration for enterprise logging +- Enhanced Grafana Faro support with improved configuration -#### Sistema de Tracking -- **EngineStatelessWidget**: Implementação otimizada para tracking automĆ”tico +### Changed +- Improved adapter pattern with better error handling +- Enhanced configuration system with validation +- Better documentation with comprehensive examples ### Fixed -- **Code Organization**: Otimização do código com redução de linhas desnecessĆ”rias +- Memory leaks in service adapters +- Initialization race conditions +- Configuration validation issues -### Dependencies -- **http: ^1.1.0**: Adicionada para requisiƧƵes HTTP no exemplo - -## [1.1.0] - 2025-01-23 +## [1.2.0] - 2024-10-15 ### Added -- **Sistema de Configuração Aprimorado**: Nova arquitetura de configuração com modelos padrĆ£o -- **Cobertura de Testes Abrangente**: 62 testes de unidade com cobertura superior a 95% nas configuraƧƵes -- **Documentação Completa**: README detalhado com exemplos prĆ”ticos de uso -- **Exemplos de View Tracking**: Sistema completo de tracking de telas e aƧƵes de usuĆ”rio - -### Enhanced -#### Configuração de Analytics -- `EngineAnalyticsModel`: Modelo principal para configuração de analytics -- `EngineAnalyticsModelDefault`: Implementação padrĆ£o com serviƧos desabilitados por seguranƧa -- `EngineFirebaseAnalyticsConfig`: Configuração especĆ­fica do Firebase Analytics -- Reutilização da configuração Faro para integração dual +- Grafana Faro integration for observability and monitoring +- Enhanced Firebase Analytics adapter with custom parameters +- Improved error tracking with context information +- Session management with automatic ID generation -#### Sistema de Analytics Refatorado -- **EngineAnalytics**: Refatorado para usar sistema de configuração baseado em modelos -- **Construtor Privado**: Implementação de padrĆ£o singleton com mĆ©todos estĆ”ticos apenas -- **Inicialização Condicional**: ServiƧos inicializam apenas quando habilitados -- **MĆ©todo Reset**: Suporte para reset de configuração (Ćŗtil para testes) - -#### Testes de Unidade -- `engine_firebase_analytics_config_test.dart`: 8 testes para configuração Firebase -- `engine_analytics_model_test.dart`: 8 testes para modelos de analytics -- `engine_analytics_test.dart`: 6 testes para funcionalidades principais -- `engine_crashlytics_config_test.dart`: 8 testes para configuração Crashlytics -- `engine_faro_config_test.dart`: 8 testes para configuração Faro -- `engine_bug_tracking_model_test.dart`: 8 testes para modelos de bug tracking -- `engine_bug_tracking_test.dart`: 6 testes para funcionalidades de bug tracking -- `engine_log_level_test.dart`: 5 testes para nĆ­veis de log -- `engine_log_test.dart`: 7 testes para sistema de logging - -#### Exemplos PrĆ”ticos -- **View Tracking Example**: Aplicação completa demonstrando tracking de views -- **Mixins de Tracking**: `EngineStatelessWidget` e `EngineStatefulWidget` -- **Tracking AutomĆ”tico**: Sistema automĆ”tico de tracking de entrada e saĆ­da de telas -- **ParĆ¢metros Customizados**: Suporte a parĆ¢metros especĆ­ficos por tela -- **Eventos de UsuĆ”rio**: Logging de aƧƵes, mudanƧas de estado e eventos customizados +### Changed +- Refactored adapter architecture for better maintainability +- Improved initialization process with better error handling +- Enhanced logging system with structured output ### Fixed -- **Null Safety**: Correção de campos `late final` para nullable evitando erros de inicialização -- **DependĆŖncias Firebase**: Isolamento adequado de dependĆŖncias para testes -- **Arquitetura Static**: Padronização de toda API pĆŗblica como mĆ©todos estĆ”ticos - -### Documentation -- **README Atualizado**: Documentação completa com: - - Instalação e configuração passo a passo - - Exemplos de uso para Analytics e Bug Tracking - - ConfiguraƧƵes avanƧadas por ambiente - - Melhores prĆ”ticas de implementação - - Suporte a todas as plataformas Flutter - -### Quality Improvements -- **Cobertura de Testes**: - - Config Files: 100% (3/3 arquivos) - - Model Files: 100% (2/2 arquivos) - - Logging: 77% (24/31 linhas) - - Cobertura Total: 33.5% (62 de 185 linhas executĆ”veis) -- **62 Testes Passando**: 100% de sucesso em todos os testes -- **Arquitetura Consistente**: Padronização com prefixo "Engine" em todas as classes -- **Type Safety**: Implementação tipo-segura em toda a biblioteca - -### Breaking Changes -- `EngineAnalyticsService` renomeado para `EngineAnalytics` (consistĆŖncia de nomenclatura) -- Remoção de providers individuais em favor do sistema de configuração baseado em modelos -- API de inicialização alterada para usar modelos de configuração +- Firebase Crashlytics initialization issues +- Analytics event parameter validation +- Memory management improvements -### Migration Guide -```dart -// Antes (v1.0.x) -await EngineAnalyticsService.initialize(/* ... */); - -// Agora (v1.1.0+) -final analyticsModel = EngineAnalyticsModel( - firebaseConfig: EngineFirebaseAnalyticsConfig( - enabled: true, - ), - faroConfig: EngineFaroConfig( - enabled: true, - endpoint: 'https://faro.example.com', - // ... outras configuraƧƵes - ), -); -await EngineAnalytics.initWithModel(analyticsModel); -``` - -## [1.0.1] - 2025-06-23 +## [1.1.0] - 2024-09-10 ### Added -- **Complete CI/CD Infrastructure**: Comprehensive GitHub Actions pipeline for automated testing, analysis, and publishing -- **Code Quality Integration**: Pana analysis with perfect 160/160 score requirement -- **Code Coverage Tracking**: Codecov integration with 49.5% coverage (45% target) -- **Professional Issue Templates**: Structured templates for bug reports and feature requests -- **Development Automation**: Scripts for automated testing and quality analysis -- **Quality Assurance**: Weekly automated security and dependency audits - -### Infrastructure Files Added -``` -.github/ -ā”œā”€ā”€ workflows/ -│ ā”œā”€ā”€ ci.yml # Main CI pipeline (tests, analysis, coverage) -│ ā”œā”€ā”€ publish.yml # Automatic pub.dev publishing on tags -│ └── quality.yml # Weekly quality and security checks -ā”œā”€ā”€ issue_template/ -│ ā”œā”€ā”€ bug_report.md # Structured bug reporting template -│ └── feature_request.md # Feature request template with priority -ā”œā”€ā”€ pull_request_template.md # Comprehensive PR review template -└── README.md # CI/CD infrastructure documentation +- Firebase Crashlytics integration for crash reporting +- Enhanced Firebase Analytics support +- Custom error tracking with stack traces +- User identification and properties management -codecov.yml # Code coverage configuration (45% target) -pana_config.yaml # Package analysis configuration (160/160 score) -scripts/ -ā”œā”€ā”€ test_coverage.sh # Automated test coverage with HTML reports -└── pana_analysis.sh # Package quality analysis script -``` +### Changed +- Improved service initialization with better error handling +- Enhanced adapter pattern implementation +- Better configuration management -### CI/CD Features -- **Automated Testing**: Complete test suite execution with coverage reporting -- **Code Quality**: Integrated Pana analysis and Flutter code analysis -- **Format Validation**: Automatic code formatting verification -- **Publishing Automation**: Tag-based automatic publishing to pub.dev -- **Security Audits**: Weekly dependency and security analysis -- **Coverage Integration**: Codecov reporting with PR comments - -### Quality Standards Achieved -- āœ… **Pana Score**: 160/160 (Perfect) -- āœ… **Tests**: 83 passing (100% success rate) -- āœ… **Coverage**: 49.5% (exceeds 45% target) -- āœ… **Linting**: 0 warnings, 0 errors -- āœ… **Formatting**: 100% compliant - -### Configuration Optimizations -- **Branch Strategy**: Streamlined to main branch workflow -- **Template Internationalization**: English templates for global accessibility -- **Publishing Method**: Direct `dart pub publish` with secure credential management -- **Quality Requirements**: Perfect Pana score enforcement +### Fixed +- Service initialization timing issues +- Analytics event tracking reliability +- Bug tracking context preservation -## [1.0.0] - 2025-01-22 +## [1.0.0] - 2024-08-01 ### Added -- Initial release of `engine_tracking` package -- **EngineAnalytics**: Complete analytics system supporting Firebase Analytics and Grafana Faro -- **EngineBugTracking**: Bug tracking system with Firebase Crashlytics and Grafana Faro integration -- **EngineLog**: Structured logging system with multiple log levels -- **Configuration Models**: Type-safe configuration classes for all services -- **Dual Integration**: Simultaneous support for Firebase and Grafana Faro services -- **Conditional Initialization**: Services initialize only when enabled in configuration -- **Static API**: All public methods are static for easy access - -### Features -#### Analytics (EngineAnalytics) -- Event logging with custom parameters -- User identification and properties -- Page/screen tracking -- App open events +- Initial release of Engine Tracking library - Firebase Analytics integration -- Grafana Faro integration - -#### Bug Tracking (EngineBugTracking) -- Error recording with stack traces -- Flutter error handling -- User identification -- Custom key-value logging -- Structured logging with levels -- Firebase Crashlytics integration -- Grafana Faro integration +- Basic bug tracking capabilities +- Type-safe Dart implementation +- iOS and Android platform support +- Unified API for multiple tracking services +- Adapter pattern for service integrations -#### Configuration -- `EngineAnalyticsModel`: Analytics configuration model -- `EngineFirebaseAnalyticsConfig`: Firebase Analytics configuration -- `EngineBugTrackingModel`: Bug tracking configuration model -- `EngineCrashlyticsConfig`: Crashlytics configuration -- `EngineFaroConfig`: Grafana Faro configuration (shared) - -#### System -- `EngineLogLevelType`: Log level enumeration -- `EngineLog`: Structured logging implementation - -### Supported Platforms -- āœ… iOS -- āœ… Android - -### Dependencies -- `firebase_core: ^3.14.0` -- `firebase_analytics: ^11.5.0` -- `firebase_crashlytics: ^4.3.7` -- `faro: ^0.3.6` +### Features +- Analytics event tracking +- User management and properties +- Screen navigation tracking +- Error reporting and logging +- Flexible service configuration -### Development -- Flutter lints for code quality -- Dart SDK compatibility: `>=3.8.0 <4.0.0` -- Flutter compatibility: `>=3.32.0` +### Platforms +- iOS support +- Android support --- -## [Unreleased] +## Contributing + +When contributing to this project, please: -### Planned Features -- Web platform support -- macOS platform support -- Windows platform support -- Linux platform support -- Advanced filtering options -- Performance monitoring integration -- Custom event validation \ No newline at end of file +1. Update the changelog for any notable changes +2. Follow semantic versioning principles +3. Include dependency updates in the Dependencies section +4. Categorize changes appropriately (Added, Changed, Fixed, Removed, Security) +5. Include the date of release in YYYY-MM-DD format \ No newline at end of file diff --git a/README.md b/README.md index 77281a4..c4201b4 100644 --- a/README.md +++ b/README.md @@ -4,316 +4,306 @@ Engine Tracking Logo -## šŸ“‹ Sobre o Projeto - [![pub.dev](https://img.shields.io/pub/v/engine_tracking.svg)](https://pub.dev/packages/engine_tracking) [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) [![Flutter](https://img.shields.io/badge/Flutter-3.32.0+-blue.svg)](https://flutter.dev/) [![Dart](https://img.shields.io/badge/Dart-3.8.0+-blue.svg)](https://dart.dev/) -Uma biblioteca Flutter completa para **tracking de analytics** e **bug reporting**, oferecendo integração com Firebase Analytics, Firebase Crashlytics, Microsoft Clarity, Grafana Faro, Splunk e Google Cloud Logging. +A comprehensive Flutter library for analytics tracking and bug reporting with unified integration across multiple services including Firebase Analytics, Firebase Crashlytics, Microsoft Clarity, Grafana Faro, Splunk, and Google Cloud Logging. -### šŸ“± Plataformas Suportadas -- āœ… iOS -- āœ… Android +**Supported Platforms:** iOS • Android ---- +## Quick Start -## šŸš€ Principais CaracterĆ­sticas - -- šŸ“Š **Analytics MĆŗltiplo**: Firebase Analytics, Microsoft Clarity, Grafana Faro, Splunk e Google Cloud Logging -- šŸ› **Bug Tracking AvanƧado**: Firebase Crashlytics, Grafana Faro e Google Cloud Logging para monitoramento completo -- 🌐 **HTTP Tracking**: Monitoramento automĆ”tico de requisiƧƵes HTTPS com mĆ©tricas detalhadas -- šŸ‘ļø **View Tracking**: Sistema automĆ”tico de tracking de telas com `EngineStatelessWidget` e `EngineStatefulWidget` -- āš™ļø **Configuração FlexĆ­vel**: Ative/desative serviƧos individualmente atravĆ©s de configuraƧƵes -- šŸ“ **Logging Estruturado**: Sistema de logs com diferentes nĆ­veis e contextos -- šŸ†” **Session ID AutomĆ”tico**: UUID v4 Ćŗnico por abertura do app para correlação de logs e analytics -- šŸ”’ **Tipo-seguro**: Implementação completamente tipada em Dart -- šŸ›”ļø **ConfiĆ”vel**: Implementação robusta e estĆ”vel para aplicaƧƵes empresariais -- šŸ—ļø **Arquitetura Consistente**: PadrĆ£o unificado entre Analytics e Bug Tracking -- šŸŽÆ **Inicialização Condicional**: ServiƧos sĆ£o inicializados apenas se habilitados na configuração -- šŸ“¦ **Export Unificado**: Todos os imports atravĆ©s de `package:engine_tracking/engine_tracking.dart` -- šŸš€ **Exemplos Completos**: Apps de demonstração com casos de uso reais (HTTP + View Tracking) +Add to your `pubspec.yaml`: ---- +```yaml +dependencies: + engine_tracking: ^1.5.0 +``` + +Initialize the library: + +```dart +import 'package:engine_tracking/engine_tracking.dart'; + +// Initialize both analytics and bug tracking +await EngineTrackingInitialize.initWithModels( + analyticsModel: EngineAnalyticsModel( + firebaseAnalyticsConfig: const EngineFirebaseAnalyticsConfig(enabled: true), + // Configure other services as needed + ), + bugTrackingModel: EngineBugTrackingModel( + crashlyticsConfig: const EngineCrashlyticsConfig(enabled: true), + // Configure other services as needed + ), +); + +// Start tracking events +await EngineAnalytics.logEvent('app_opened'); +``` + +## Features + +**Analytics Integration** +- Firebase Analytics for comprehensive user behavior tracking +- Microsoft Clarity with dual integration (adapter + widget) for session recordings, heatmaps, and custom events +- Grafana Faro for observability and monitoring +- Splunk for enterprise logging and analytics +- Google Cloud Logging for centralized log management -## šŸ—ŗļø Arquitetura da Solução +**Bug Tracking & Monitoring** +- Firebase Crashlytics for crash reporting +- Automatic Flutter error handling +- Custom error tracking with context +- Structured logging with multiple severity levels -### šŸ†” Sistema de Session ID (Correlação AutomĆ”tica) +**Developer Experience** +- Type-safe implementation with full null safety +- Automatic session ID correlation across all services +- Flexible service configuration (enable/disable individually) +- Custom widgets with built-in tracking capabilities +- HTTP request monitoring +- Single import entry point + +## Architecture + +Engine Tracking uses an adapter pattern to provide unified interfaces for multiple analytics and bug tracking services. Each service can be enabled or disabled independently through configuration. + +### Session Management + +```mermaid +graph LR + A[App Start] --> B[Generate UUID] + B --> C[Session ID] + C --> D[Analytics Events] + C --> E[Bug Reports] + C --> F[Log Entries] +``` + +All tracking events automatically include a unique session ID for correlation across services. + +### Analytics Flow ```mermaid graph TD - A["App Initialization"] --> B["EngineSession.instance"] - B --> C["Generate UUID v4"] - C --> D["xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx"] - - D --> E["Session ID em Memória"] - E --> F["Auto-inject AutomĆ”tico"] - - G["EngineAnalytics.logEvent()"] --> F - H["EngineBugTracking.log()"] --> F - I["EngineLog.info()"] --> F - J["Firebase Analytics"] --> F - K["Google Cloud Logging"] --> F - L["Crashlytics"] --> F - - F --> M["Enrich Data"] - M --> N["session_id: UUID v4"] - - N --> O["Firebase Analytics"] - N --> P["Google Cloud Logging"] - N --> Q["Grafana Faro"] - N --> R["Splunk"] - N --> S["Crashlytics"] - - U["Correlação de Logs"] --> V["Mesmo session_id"] - V --> W["Jornada Completa do UsuĆ”rio"] + A[Your App] --> B[EngineAnalytics] + B --> C[Adapters] + C --> D[Firebase Analytics] + C --> E[Microsoft Clarity] + C --> F[Grafana Faro] + C --> G[Splunk] + C --> H[Google Cloud Logging] ``` -### šŸ“± Widgets Stateless e Stateful com Tracking AutomĆ”tico +### Bug Tracking Flow ```mermaid graph TD - A["App"] --> B["EngineStatelessWidget"] - A --> C["EngineStatefulWidget"] - - B --> D["buildWithTracking()"] - C --> E["buildWithTracking()"] - - B --> F["MĆ©todos ExecutĆ”veis"] - C --> G["MĆ©todos ExecutĆ”veis"] - - F --> H["logUserAction()"] - F --> I["logCustomEvent()"] - F --> J["logScreenError()"] - - G --> K["logUserAction()"] - G --> L["logCustomEvent()"] - G --> M["logScreenError()"] - G --> N["logStateChange()"] - - H --> O["EngineLog.debug()"] - I --> O - J --> P["EngineLog.error()"] - K --> O - L --> O - M --> P - N --> O - - B --> Q["Lifecycle Tracking"] - C --> R["Lifecycle Tracking"] - - Q --> S["screen_initialized"] - Q --> T["screen_viewed"] - Q --> U["screen_closed"] - - R --> V["screen_initialized"] - R --> W["screen_viewed"] - R --> X["screen_closed"] - - S --> O - T --> O - U --> O - V --> O - W --> O - X --> O + A[Flutter Errors] --> B[EngineBugTracking] + A2[Custom Errors] --> B + B --> C[Adapters] + C --> D[Firebase Crashlytics] + C --> E[Grafana Faro] + C --> F[Google Cloud Logging] ``` -### šŸ“ Sistema de Logging (EngineLog) +### EngineLog - Unified Logging System + +`EngineLog` is the central logging system that automatically abstracts and unifies calls to both Analytics and Bug Tracking services. It's the recommended way to log events as it handles service routing automatically. ```mermaid graph TD - A["Aplicação"] --> B["EngineLog"] - - B --> C["debug()"] - B --> D["info()"] - B --> E["warning()"] - B --> F["error()"] - B --> G["fatal()"] - - C --> H["_logWithLevel()"] - D --> H - E --> H - F --> H - G --> H - - H --> I["developer.log()"] - H --> J{{"EngineAnalytics.isEnabled && includeInAnalytics?"}} - H --> K{{"EngineBugTracking.isEnabled?"}} - - J -->|Sim| L["EngineAnalytics.logEvent()"] - J -->|NĆ£o| M["Skip Analytics"] - - K -->|Sim| N["EngineBugTracking.log()"] - K -->|NĆ£o| O["Skip Bug Tracking"] - - L --> P["Firebase Analytics"] - L --> Q["Grafana Faro"] - L --> R["Splunk"] - - N --> T["Firebase Crashlytics"] - N --> U["Grafana Faro Bug Tracking"] - - K -->|Sim| V{{"level == error || fatal?"}} - V -->|Sim| W["EngineBugTracking.recordError()"] - V -->|NĆ£o| X["Apenas log normal"] - - W --> Y["Crash Reporting"] - Y --> T - Y --> U + A[Your App] --> B[EngineLog] + B --> C{Log Level} + C -->|debug/info| D[Developer Console] + C -->|warning/error/fatal| E[Developer Console + Services] + E --> F[EngineAnalytics] + E --> G[EngineBugTracking] + F --> H[All Analytics Services] + G --> I[All Bug Tracking Services] ``` -### šŸ“Š Sistema de Analytics (EngineAnalytics) +**Key Benefits:** +- **Single API**: One method call routes to multiple services +- **Automatic Service Detection**: Only calls enabled services +- **Level-based Routing**: Different log levels go to appropriate services +- **Context Preservation**: Maintains session ID and metadata across all services +- **Performance Optimized**: No-op when services are disabled + +### Widget Tracking System + +Engine Tracking provides custom widgets that automatically track user interactions and screen lifecycle events. ```mermaid graph TD - A["Aplicação"] --> B["EngineAnalytics"] - - B --> C["init()"] - B --> D["logEvent()"] - B --> E["setUserId()"] - B --> F["setUserProperty()"] - B --> G["setPage()"] - B --> H["logAppOpen()"] - - C --> I["EngineAnalyticsModel"] - I --> J["Firebase Analytics Config"] - I --> K["Faro Config"] - I --> L["Splunk Config"] - I --> M["Google Logging Config"] - - D --> O["Adapters"] - E --> O - F --> O - G --> O - H --> O - - O --> P["EngineFirebaseAnalyticsAdapter"] - O --> Q["EngineFaroAnalyticsAdapter"] - O --> R["EngineSplunkAnalyticsAdapter"] - O --> S["EngineGoogleLoggingAnalyticsAdapter"] + A[EngineStatelessWidget] --> B[buildWithTracking] + C[EngineStatefulWidget] --> D[buildWithTracking] + B --> E[Automatic Screen Tracking] + D --> E + E --> F[screen_initialized] + E --> G[screen_viewed] + E --> H[screen_disposed] - P --> T["Firebase Analytics SDK"] - Q --> U["Grafana Faro SDK"] - R --> V["Splunk SDK"] - S --> W["Google Cloud Logging API"] + I[User Actions] --> J[logUserAction] + K[State Changes] --> L[logStateChange] + M[Custom Events] --> N[logCustomEvent] - T --> X["Google Analytics Dashboard"] - U --> Y["Grafana Dashboard"] - V --> Z["Splunk Dashboard"] - W --> AA["Google Cloud Console"] + J --> O[EngineLog] + L --> O + N --> O +``` + +### Navigation Observer + +Automatic navigation tracking with `EngineNavigationObserver`: + +```mermaid +graph LR + A[Navigator] --> B[EngineNavigationObserver] + B --> C[Route Changes] + C --> D[EngineAnalytics.setPage] + D --> E[All Analytics Services] ``` -### šŸ› Sistema de Bug Tracking (EngineBugTracking) +### HTTP Tracking System + +Engine Tracking provides comprehensive HTTP request/response logging through `EngineHttpOverride` and `EngineHttpTracking`. ```mermaid graph TD - A["Aplicação"] --> B["EngineBugTracking"] - - B --> C["init()"] - B --> D["log()"] - B --> E["recordError()"] - B --> F["recordFlutterError()"] - B --> G["setCustomKey()"] - B --> H["setUserIdentifier()"] - B --> I["testCrash()"] - - C --> J["EngineBugTrackingModel"] - J --> K["Crashlytics Config"] - J --> L["Faro Config"] - J --> M["Google Logging Config"] - - D --> N["Adapters"] - E --> N - F --> N - G --> N - H --> N - I --> N - - N --> O["EngineCrashlyticsAdapter"] - N --> P["EngineFaroBugTrackingAdapter"] - N --> Q["EngineGoogleLoggingBugTrackingAdapter"] - - O --> R["Firebase Crashlytics SDK"] - P --> S["Grafana Faro SDK"] - Q --> T["Google Cloud Logging API"] - - R --> U["Firebase Console"] - S --> V["Grafana Dashboard"] - T --> W["Google Cloud Console"] - - X["Flutter Error Handler"] --> F - Y["Platform Error Handler"] --> E - - Z["Custom Errors"] --> E - AA["Logging Events"] --> D + A[HTTP Requests] --> B[EngineHttpOverride] + B --> C[Request Logging] + B --> D[Response Logging] + B --> E[Error Logging] + C --> F[EngineLog.debug] + D --> F + E --> G[EngineLog.error] + F --> H[All Tracking Services] + G --> H ``` ---- +**Key Features:** +- **Automatic HTTP Logging**: Intercepts all HTTP requests/responses +- **Configurable Logging**: Control what gets logged (headers, body, timing) +- **Error Tracking**: Automatic error logging for failed requests +- **Performance Metrics**: Request timing and performance data +- **Chain-friendly**: Works with existing HttpOverrides (like FaroHttpOverrides) +- **Multiple Configurations**: Development, production, and error-only presets -## šŸ“¦ Instalação +**Configuration Options:** +```dart +// Development configuration (verbose logging) +const EngineHttpTrackingConfig( + enabled: true, + enableRequestLogging: true, + enableResponseLogging: true, + enableTimingLogging: true, + enableHeaderLogging: true, + enableBodyLogging: true, + maxBodyLogLength: 2000, + logName: 'HTTP_TRACKING_DEV', +) + +// Production configuration (minimal logging) +const EngineHttpTrackingConfig( + enabled: true, + enableRequestLogging: true, + enableResponseLogging: true, + enableTimingLogging: true, + enableHeaderLogging: false, // Be careful with sensitive data + enableBodyLogging: false, // Be careful with sensitive data + maxBodyLogLength: 500, + logName: 'HTTP_TRACKING_PROD', +) + +// Error-only configuration (only log failures) +const EngineHttpTrackingConfig( + enabled: true, + enableRequestLogging: false, + enableResponseLogging: true, + enableTimingLogging: true, + enableHeaderLogging: false, + enableBodyLogging: false, + maxBodyLogLength: 0, + logName: 'HTTP_ERRORS', +) + +// Custom configuration +const EngineHttpTrackingConfig( + enabled: true, + enableRequestLogging: true, + enableResponseLogging: true, + enableTimingLogging: true, + enableHeaderLogging: false, // Be careful with sensitive data + enableBodyLogging: false, // Be careful with sensitive data + maxBodyLogLength: 1000, + logName: 'CUSTOM_HTTP', +) +``` + +## Installation -Adicione ao seu `pubspec.yaml`: +Add to your `pubspec.yaml`: ```yaml dependencies: - engine_tracking: ^1.4.0 + engine_tracking: ^1.5.0 ``` -Execute: +Run: ```bash flutter pub get ``` ---- - -## šŸš€ Exemplos de Uso +## Usage Examples -### šŸ“± Exemplos Inclusos +### Complete Examples -O pacote inclui exemplos completos demonstrando todas as funcionalidades: +The package includes complete examples demonstrating all functionality: ```bash cd example && flutter run ``` -- **šŸ“± Exemplo Principal**: Inicialização, tracking de eventos, propriedades de usuĆ”rio e navegação -- **🌐 Exemplo HTTP Tracking**: RequisiƧƵes com PokĆ©API e JSONPlaceholder -- **šŸ‘ļø Exemplo View Tracking**: Sistema automĆ”tico de tracking de telas +Available examples: +- **Main Example**: Initialization, event tracking, user properties, and navigation +- **HTTP Tracking**: Requests with PokĆ©API and JSONPlaceholder +- **View Tracking**: Automatic screen tracking system -### šŸš€ Inicialização Centralizada (Recomendado) +### Centralized Initialization (Recommended) -**Novo!** Use o `EngineTrackingInitialize` para inicializar Analytics e Bug Tracking de uma só vez: +Use `EngineTrackingInitialize` to initialize both Analytics and Bug Tracking: ```dart import 'package:engine_tracking/engine_tracking.dart'; -// Ambos os serviƧos +// Both services await EngineTrackingInitialize.initWithModels( analyticsModel: EngineAnalyticsModel(/* configs */), bugTrackingModel: EngineBugTrackingModel(/* configs */), ); -// Apenas Analytics +// Analytics only await EngineTrackingInitialize.initWithModels( analyticsModel: EngineAnalyticsModel(/* configs */), bugTrackingModel: null, ); -// Apenas Bug Tracking +// Bug tracking only await EngineTrackingInitialize.initWithModels( analyticsModel: null, bugTrackingModel: EngineBugTrackingModel(/* configs */), ); -// Com Adapters (controle granular) +// With adapters (granular control) await EngineTrackingInitialize.initWithAdapters( analyticsAdapters: [EngineFirebaseAnalyticsAdapter(/* config */)], bugTrackingAdapters: null, // Skip bug tracking ); -// Inicialização rĆ”pida (ambos desabilitados) +// Quick initialization (both disabled) await EngineTrackingInitialize.initWithDefaults(); // Status @@ -324,7 +314,84 @@ bool anyEnabled = EngineTrackingInitialize.isEnabled; await EngineTrackingInitialize.dispose(); ``` -### šŸŽÆ Configuração BĆ”sica (MĆ©todo Individual) +### Microsoft Clarity Integration + +Engine Tracking provides two ways to integrate with Microsoft Clarity: + +#### 1. Adapter-based Integration (Recommended) + +The `EngineClarityAdapter` integrates Clarity into the unified analytics system: + +```dart +final analyticsModel = EngineAnalyticsModel( + clarityConfig: const EngineClarityConfig( + enabled: true, + projectId: 'your-clarity-project-id', + ), + // ... other configs +); + +await EngineAnalytics.initWithModel(analyticsModel); + +// All analytics calls now include Clarity +await EngineAnalytics.logEvent('user_action'); +await EngineAnalytics.setUserId('user_123'); +await EngineAnalytics.setPage('home_screen'); +``` + +#### 2. Widget-based Integration (For Session Recordings) + +Use `EngineWidget` to automatically enable session recordings and heatmaps. The widget automatically detects if Clarity has been initialized through the analytics system - no manual configuration required: + +```dart +void main() async { + WidgetsFlutterBinding.ensureInitialized(); + + // Initialize analytics with Clarity configuration + final analyticsModel = EngineAnalyticsModel( + clarityConfig: EngineClarityConfig( + enabled: true, + projectId: 'your-clarity-project-id', + ), + // ... other configurations + ); + + await EngineAnalytics.initWithModel(analyticsModel); + + // EngineWidget automatically detects and uses Clarity configuration + // No need to pass clarityConfig manually! + runApp(EngineWidget(app: MyApp())); +} +``` + +#### Combined Usage (Best Practice) + +For complete Clarity integration, initialize analytics and use EngineWidget: + +```dart +void main() async { + WidgetsFlutterBinding.ensureInitialized(); + + final clarityConfig = EngineClarityConfig( + enabled: true, + projectId: 'your-clarity-project-id', + ); + + // Initialize analytics with Clarity adapter + await EngineAnalytics.initWithModel( + EngineAnalyticsModel( + clarityConfig: clarityConfig, + // ... other configs + ), + ); + + // EngineWidget automatically detects Clarity configuration + // No manual configuration needed! + runApp(EngineWidget(app: MyApp())); +} +``` + +### Individual Service Configuration ```dart import 'package:engine_tracking/engine_tracking.dart'; @@ -334,31 +401,31 @@ Future setupTracking() async { firebaseAnalyticsConfig: const EngineFirebaseAnalyticsConfig(enabled: true), clarityConfig: const EngineClarityConfig( enabled: true, - projectId: 'seu-projeto-clarity', + projectId: 'your-clarity-project-id', ), faroConfig: const EngineFaroConfig( enabled: true, endpoint: 'https://faro-collector.grafana.net/collect', - appName: 'MeuApp', + appName: 'YourApp', appVersion: '1.0.0', environment: 'production', - apiKey: 'sua-chave-api-faro', + apiKey: 'your-faro-api-key', ), googleLoggingConfig: const EngineGoogleLoggingConfig( enabled: true, - projectId: 'seu-projeto-gcp', + projectId: 'your-gcp-project', logName: 'engine-tracking', credentials: { "type": "service_account", - "project_id": "seu-projeto-gcp", + "project_id": "your-gcp-project", "private_key_id": "...", "private_key": "-----BEGIN PRIVATE KEY-----\n...\n-----END PRIVATE KEY-----\n", - "client_email": "sua-service-account@seu-projeto-gcp.iam.gserviceaccount.com", - // ... resto das credenciais + "client_email": "your-service-account@your-gcp-project.iam.gserviceaccount.com", + // ... rest of credentials }, resource: { 'type': 'global', - 'labels': {'project_id': 'seu-projeto-gcp'}, + 'labels': {'project_id': 'your-gcp-project'}, }, ), splunkConfig: const EngineSplunkConfig(enabled: false), @@ -369,10 +436,10 @@ Future setupTracking() async { faroConfig: const EngineFaroConfig( enabled: true, endpoint: 'https://faro-collector.grafana.net/collect', - appName: 'MeuApp', + appName: 'YourApp', appVersion: '1.0.0', environment: 'production', - apiKey: 'sua-chave-api-faro', + apiKey: 'your-faro-api-key', ), googleLoggingConfig: const EngineGoogleLoggingConfig(enabled: true, /* configs */), ); @@ -382,104 +449,130 @@ Future setupTracking() async { } ``` -### šŸ“ˆ Tracking de Eventos +### Event Tracking ```dart -// Evento simples (Session ID incluĆ­do automaticamente) +// Simple event (Session ID included automatically) await EngineAnalytics.logEvent('button_clicked'); -// Evento com parĆ¢metros +// Event with parameters await EngineAnalytics.logEvent('purchase_completed', { 'item_id': 'premium_plan', 'value': 29.99, - 'currency': 'BRL', + 'currency': 'USD', 'category': 'subscription', }); -// Evento de abertura do app +// App open event await EngineAnalytics.logAppOpen(); ``` -### šŸ‘¤ Gerenciamento de UsuĆ”rio +### User Management ```dart -// Definir ID do usuĆ”rio +// Set user ID await EngineAnalytics.setUserId('user_12345'); -// Com informaƧƵes completas (para Faro/Clarity) +// With complete information (for Faro/Clarity) await EngineAnalytics.setUserId( 'user_12345', - 'usuario@exemplo.com', - 'JoĆ£o Silva', + 'user@example.com', + 'John Doe', ); -// Propriedades do usuĆ”rio +// User properties await EngineAnalytics.setUserProperty('user_type', 'premium'); await EngineAnalytics.setUserProperty('plan', 'monthly'); ``` -### 🧭 Navegação de Telas +### Screen Navigation ```dart -// Tela simples +// Simple screen await EngineAnalytics.setPage('HomeScreen'); -// Com contexto completo +// With complete context await EngineAnalytics.setPage( - 'ProductScreen', // Tela atual - 'HomeScreen', // Tela anterior - 'ECommerceApp', // Classe da tela + 'ProductScreen', // Current screen + 'HomeScreen', // Previous screen + 'ECommerceApp', // Screen class ); ``` -### šŸ› Bug Tracking +### Bug Tracking ```dart -// Log estruturado -await EngineBugTracking.log('UsuĆ”rio realizou compra', { +// Structured logging +await EngineBugTracking.log('User completed purchase', { 'user_id': '12345', 'product_id': 'abc-123', 'amount': 29.99, }); -// Capturar erros +// Capture errors try { - // código que pode falhar + // code that might fail } catch (error, stackTrace) { await EngineBugTracking.recordError( error, stackTrace, - reason: 'Falha no processamento de pagamento', + reason: 'Payment processing failure', ); } -// Definir contexto do usuĆ”rio -await EngineBugTracking.setUserIdentifier('user_12345'); +// Set user context +await EngineBugTracking.setUserIdentifier('user_12345', 'user@example.com', 'John Doe'); await EngineBugTracking.setCustomKey('subscription_plan', 'premium'); ``` -### šŸ“ Sistema de Logging +### Logging System ```dart -// Diferentes nĆ­veis de log -EngineLog.debug('Debug information'); -EngineLog.info('Informational message'); -EngineLog.warning('Warning message'); -EngineLog.error('Error occurred'); -EngineLog.fatal('Fatal error'); - -// Com contexto adicional -EngineLog.info('User action', context: { +// Different log levels +await EngineLog.debug('Debug information'); +await EngineLog.info('Informational message'); +await EngineLog.warning('Warning message'); +await EngineLog.error('Error occurred'); +await EngineLog.fatal('Fatal error'); + +// With additional data +await EngineLog.info('User action', data: { 'action': 'button_click', 'screen': 'home', 'user_id': '12345', }); -// Incluir em analytics (padrĆ£o: false para debug/info) -EngineLog.warning('Important warning', includeInAnalytics: true); +// Custom log name and include in analytics +await EngineLog.warning('Important warning', + logName: 'USER_ACTION', + includeInAnalytics: true, +); + +// Error logging with exception and stack trace +try { + // risky operation +} catch (error, stackTrace) { + await EngineLog.error('Operation failed', + error: error, + stackTrace: stackTrace, + data: {'operation': 'user_registration'}, + ); +} + +// Fatal error with complete context +await EngineLog.fatal('Critical system failure', + logName: 'SYSTEM', + error: exception, + stackTrace: stackTrace, + data: { + 'component': 'payment_processor', + 'user_id': 'user_123', + 'transaction_id': 'tx_456', + }, +); ``` -### šŸ‘ļø View Tracking com Widgets +### View Tracking with Widgets ```dart class HomePage extends EngineStatelessWidget { @@ -544,7 +637,7 @@ class _ShoppingCartPageState extends EngineStatefulWidgetState @override Widget buildWithTracking(BuildContext context) { return Scaffold( - appBar: AppBar(title: const Text('Carrinho')), + appBar: AppBar(title: const Text('Shopping Cart')), body: ListView.builder( itemCount: _products.length, itemBuilder: (context, index) { @@ -591,62 +684,498 @@ class _ShoppingCartPageState extends EngineStatefulWidgetState } ``` -### āœ… Verificação de Status +### Custom Widgets + +Engine Tracking provides several specialized widgets for enhanced tracking capabilities: + +#### EngineWidget - Root App Wrapper + +Automatically wraps your app to enable Microsoft Clarity session recordings when Clarity is initialized. The widget intelligently detects if Clarity has been configured through the analytics system and automatically enables session recordings without requiring manual configuration: ```dart -// Verificar se analytics estĆ” habilitado +void main() async { + WidgetsFlutterBinding.ensureInitialized(); + + // Initialize analytics with Clarity configuration + await EngineAnalytics.initWithModel( + EngineAnalyticsModel( + clarityConfig: EngineClarityConfig( + enabled: true, + projectId: 'your-clarity-project-id', + ), + firebaseAnalyticsConfig: const EngineFirebaseAnalyticsConfig(enabled: true), + // ... other configs + ), + ); + + // EngineWidget automatically detects and enables Clarity session recordings + // No manual clarityConfig parameter needed! + runApp(EngineWidget(app: MyApp())); +} + +class MyApp extends StatelessWidget { + @override + Widget build(BuildContext context) { + return MaterialApp( + home: HomePage(), + navigatorObservers: [EngineNavigationObserver()], + ); + } +} + +class HomePage extends StatelessWidget { + @override + Widget build(BuildContext context) { + return Scaffold( + body: Column( + children: [ + ElevatedButton( + onPressed: () async { + // This event goes to all analytics services including Clarity + await EngineAnalytics.logEvent('button_clicked', { + 'button_type': 'primary', + 'screen': 'home', + }); + }, + child: Text('Track Event'), + ), + + // Mask sensitive content in Clarity recordings + EngineMaskWidget( + child: Text('Sensitive user data'), + ), + ], + ), + ); + } +} +``` + +#### EngineMaskWidget - Privacy Protection + +Masks sensitive content in session recordings: + +```dart +class PaymentScreen extends StatelessWidget { + @override + Widget build(BuildContext context) { + return Scaffold( + body: Column( + children: [ + Text('Order Summary'), + + // Mask sensitive payment information + EngineMaskWidget( + child: Column( + children: [ + Text('Credit Card: **** **** **** 1234'), + Text('CVV: 123'), + + // Unmask non-sensitive info within masked area + EngineUnmaskWidget( + child: Text('Expires: 12/25'), + ), + ], + ), + ), + + ElevatedButton( + onPressed: () => processPayment(), + child: Text('Pay Now'), + ), + ], + ), + ); + } +} +``` + +#### EngineStatelessWidget - Enhanced Stateless Widgets + +Provides automatic screen tracking and user action logging: + +```dart +class ProductListPage extends EngineStatelessWidget { + ProductListPage({super.key}); + + @override + String get screenName => 'product_list'; + + @override + Map? get screenParameters => { + 'category': 'electronics', + 'sort_by': 'price', + }; + + @override + Widget buildWithTracking(BuildContext context) { + return Scaffold( + appBar: AppBar(title: Text('Products')), + body: ListView.builder( + itemBuilder: (context, index) { + return ListTile( + title: Text('Product $index'), + onTap: () { + // Automatically tracked user action + logUserAction('product_selected', parameters: { + 'product_id': index, + 'product_name': 'Product $index', + }); + + // Navigate to product details + Navigator.push(context, + MaterialPageRoute(builder: (_) => ProductDetailPage())); + }, + ); + }, + ), + floatingActionButton: FloatingActionButton( + onPressed: () { + // Log custom events + logCustomEvent('filter_opened'); + }, + child: Icon(Icons.filter_list), + ), + ); + } +} +``` + +#### EngineStatefulWidget - Enhanced Stateful Widgets + +Includes state change tracking in addition to all EngineStatelessWidget features: + +```dart +class ShoppingCartPage extends StatefulWidget { + @override + State createState() => _ShoppingCartPageState(); +} + +class _ShoppingCartPageState extends EngineStatefulWidgetState { + List _items = []; + + @override + String get screenName => 'shopping_cart'; + + @override + Map? get screenParameters => { + 'initial_item_count': _items.length, + 'cart_value': _calculateTotal(), + }; + + @override + Widget buildWithTracking(BuildContext context) { + return Scaffold( + appBar: AppBar(title: Text('Shopping Cart')), + body: ListView.builder( + itemCount: _items.length, + itemBuilder: (context, index) { + return ListTile( + title: Text(_items[index].name), + trailing: IconButton( + icon: Icon(Icons.remove), + onPressed: () => _removeItem(index), + ), + ); + }, + ), + ); + } + + void _removeItem(int index) { + final removedItem = _items[index]; + + setState(() { + _items.removeAt(index); + }); + + // Track user action + logUserAction('item_removed', parameters: { + 'item_id': removedItem.id, + 'remaining_items': _items.length, + }); + + // Track state change + logStateChange('cart_updated', additionalData: { + 'action': 'item_removal', + 'new_total': _calculateTotal(), + }); + } + + double _calculateTotal() { + return _items.fold(0.0, (sum, item) => sum + item.price); + } +} +``` + +### Navigation Observer + +Automatic navigation tracking with `EngineNavigationObserver`: + +```dart +class MyApp extends StatelessWidget { + @override + Widget build(BuildContext context) { + return MaterialApp( + title: 'My App', + // Add the navigation observer for automatic screen tracking + navigatorObservers: [ + EngineNavigationObserver(), + ], + home: HomePage(), + routes: { + '/products': (context) => ProductListPage(), + '/cart': (context) => ShoppingCartPage(), + '/profile': (context) => ProfilePage(), + }, + ); + } +} +``` + +The `EngineNavigationObserver` automatically: +- Tracks route changes and screen transitions +- Calls `EngineAnalytics.setPage()` with screen names +- Provides navigation context for analytics +- Works seamlessly with both named routes and direct navigation + +### HTTP Request Tracking + +Engine Tracking provides comprehensive HTTP request/response logging: + +```dart +import 'package:engine_tracking/engine_tracking.dart'; +import 'package:http/http.dart' as http; + +void main() async { + // Initialize Engine Tracking + await EngineTrackingInitialize.initWithModels( + analyticsModel: myAnalyticsModel, + bugTrackingModel: myBugTrackingModel, + // Add HTTP tracking configuration + httpTrackingConfig: const EngineHttpTrackingConfig( + enabled: true, + enableRequestLogging: true, + enableResponseLogging: true, + enableTimingLogging: true, + enableHeaderLogging: true, + enableBodyLogging: true, + maxBodyLogLength: 2000, + logName: 'HTTP_TRACKING_DEV', + ), + ); + + runApp(MyApp()); +} + +// HTTP requests are now automatically logged +class ApiService { + Future getUser(String userId) async { + // This request will be automatically logged with: + // - Request details (method, URL, headers, timing) + // - Response details (status, headers, timing) + // - Error details (if request fails) + final response = await http.get( + Uri.parse('https://api.example.com/users/$userId'), + headers: {'Authorization': 'Bearer $token'}, + ); + + if (response.statusCode == 200) { + return User.fromJson(json.decode(response.body)); + } else { + throw ApiException('Failed to load user'); + } + } + + Future updateUser(User user) async { + // POST requests are also automatically tracked + final response = await http.post( + Uri.parse('https://api.example.com/users/${user.id}'), + headers: { + 'Content-Type': 'application/json', + 'Authorization': 'Bearer $token', + }, + body: json.encode(user.toJson()), + ); + + if (response.statusCode != 200) { + throw ApiException('Failed to update user'); + } + } +} +``` + +**Configuration Examples:** + +```dart +// Development: Full logging with headers and body +const devConfig = EngineHttpTrackingConfig( + enabled: true, + enableRequestLogging: true, + enableResponseLogging: true, + enableTimingLogging: true, + enableHeaderLogging: true, + enableBodyLogging: true, + maxBodyLogLength: 2000, + logName: 'API_DEV', +); + +// Production: Minimal logging, no sensitive data +const prodConfig = EngineHttpTrackingConfig( + enabled: true, + enableRequestLogging: true, + enableResponseLogging: true, + enableTimingLogging: true, + enableHeaderLogging: false, // Disable for security + enableBodyLogging: false, // Disable for security + maxBodyLogLength: 500, + logName: 'API_PROD', +); + +// Error-only: Only log failed requests +const errorConfig = EngineHttpTrackingConfig( + enabled: true, + enableRequestLogging: false, + enableResponseLogging: true, + enableTimingLogging: true, + enableHeaderLogging: false, + enableBodyLogging: false, + maxBodyLogLength: 0, + logName: 'API_ERRORS', +); + +// Custom configuration +const customConfig = EngineHttpTrackingConfig( + enabled: true, + enableRequestLogging: true, + enableResponseLogging: true, + enableTimingLogging: true, + enableHeaderLogging: false, // Disable for security + enableBodyLogging: false, // Disable for security + maxBodyLogLength: 500, + logName: 'CUSTOM_API', +); +``` + +**Advanced Usage:** + +```dart +// Temporarily use different configuration +await EngineHttpTracking.withConfig( + const EngineHttpTrackingConfig( + enabled: true, + enableRequestLogging: true, + enableResponseLogging: true, + enableTimingLogging: true, + enableHeaderLogging: true, + enableBodyLogging: true, + maxBodyLogLength: 2000, + logName: 'DEBUG_HTTP', + ), + () async { + // All HTTP requests in this block use development config + await apiService.debugEndpoint(); + }, +); + +// Temporarily disable HTTP tracking +final restore = EngineHttpTracking.temporaryDisable(); +await sensitiveApiCall(); // This won't be logged +restore(); // Re-enable with previous config + +// Chain with existing HttpOverrides (like FaroHttpOverrides) +final config = EngineHttpTrackingConfig( + enabled: true, + enableRequestLogging: true, + enableResponseLogging: true, + enableTimingLogging: true, + enableHeaderLogging: false, + enableBodyLogging: false, + maxBodyLogLength: 500, + logName: 'HTTP_TRACKING_PROD', + baseOverride: FaroHttpOverrides(existingOverride), +); +EngineHttpTracking.initialize(config); + +// Log custom HTTP-related events +await EngineHttpTracking.logCustomEvent( + 'API rate limit reached', + data: { + 'endpoint': '/api/users', + 'retry_after': 60, + 'request_count': 100, + }, +); + +// Get HTTP tracking statistics +final stats = EngineHttpTracking.getStats(); +print('HTTP Tracking enabled: ${stats['is_enabled']}'); +``` + +### Status Verification + +```dart +// Check if analytics is enabled if (EngineAnalytics.isEnabled) { - print('āœ… Analytics estĆ” ativo'); + print('āœ… Analytics is active'); +} + +// Check specific services +if (EngineAnalytics.isFirebaseInitialized) { + print('šŸ”„ Firebase Analytics active'); +} + +if (EngineAnalytics.isClarityInitialized) { + print('šŸ‘ļø Microsoft Clarity active'); } -// Verificar serviƧos especĆ­ficos -if (EngineAnalytics.isFirebaseAnalyticsEnabled) { - print('šŸ”„ Firebase Analytics ativo'); +if (EngineAnalytics.isFaroInitialized) { + print('šŸ“Š Grafana Faro active'); } -if (EngineAnalytics.isFaroEnabled) { - print('šŸ“Š Faro Analytics ativo'); +if (EngineAnalytics.isSplunkInitialized) { + print('šŸ” Splunk active'); } if (EngineAnalytics.isGoogleLoggingInitialized) { - print('ā˜ļø Google Cloud Logging ativo'); + print('ā˜ļø Google Cloud Logging active'); } ``` --- -## šŸ¤ Como Contribuir +## Contributing -ContribuiƧƵes sĆ£o bem-vindas! Por favor: +Contributions are welcome! Please: -1. Fork o projeto -2. Crie uma branch para sua feature (`git checkout -b feature/AmazingFeature`) -3. Commit suas mudanƧas (`git commit -m 'Add some AmazingFeature'`) -4. Push para a branch (`git push origin feature/AmazingFeature`) -5. Abra um Pull Request +1. Fork the project +2. Create a feature branch (`git checkout -b feature/AmazingFeature`) +3. Commit your changes (`git commit -m 'Add some AmazingFeature'`) +4. Push to the branch (`git push origin feature/AmazingFeature`) +5. Open a Pull Request -### šŸ“‹ Diretrizes de Contribuição +### Contribution Guidelines -- Siga o padrĆ£o de código existente -- Documente novas funcionalidades -- Valide em Android e iOS -- Atualize o CHANGELOG.md +- Follow existing code patterns +- Document new features +- Test on both Android and iOS +- Update the CHANGELOG.md -### šŸ“„ LicenƧa +### License -Este projeto estĆ” licenciado sob a LicenƧa MIT - veja o arquivo LICENSE para detalhes. +This project is licensed under the MIT License - see the LICENSE file for details. --- -## šŸ¢ Sobre a STMR +## About STMR -Desenvolvido pela **STMR** - Especialistas em soluƧƵes móveis. +Developed by **STMR** - Mobile solutions specialists. -A STMR Ć© uma empresa focada no desenvolvimento de soluƧƵes tecnológicas inovadoras para dispositivos móveis, especializando-se em arquiteturas robustas, performance otimizada e experiĆŖncias de usuĆ”rio excepcionais. +STMR is a company focused on developing innovative technology solutions for mobile devices, specializing in robust architectures, optimized performance, and exceptional user experiences. -### šŸŽÆ Nossa MissĆ£o -Fornecer ferramentas e bibliotecas Flutter de alta qualidade que aceleram o desenvolvimento de aplicaƧƵes móveis enterprise, mantendo os mais altos padrƵes de seguranƧa, performance e usabilidade. +### Our Mission +To provide high-quality Flutter tools and libraries that accelerate enterprise mobile application development while maintaining the highest standards of security, performance, and usability. --- -**šŸ’” Dica v1.4.0**: Para mĆ”xima eficiĆŖncia, configure apenas os serviƧos que vocĆŖ realmente utiliza. A biblioteca Ć© otimizada para funcionar com qualquer combinação de serviƧos habilitados ou desabilitados. Com **Session ID automĆ”tico**, vocĆŖ agora tem correlação completa de logs e centralização avanƧada! šŸ†”šŸ”„ \ No newline at end of file +**Tip**: For maximum efficiency, configure only the services you actually use. The library is optimized to work with any combination of enabled or disabled services. With automatic Session ID, you now have complete log correlation and advanced centralization! \ No newline at end of file diff --git a/example/analysis_options.yaml b/example/analysis_options.yaml index f9b3034..a09a058 100644 --- a/example/analysis_options.yaml +++ b/example/analysis_options.yaml @@ -1 +1,208 @@ include: package:flutter_lints/flutter.yaml + +formatter: + page_width: 120 + trailing_commas: preserve + +analyzer: + exclude: + - lib/generated/** + - lib/translations/** + - lib/l10n/** + - lib/**/*.g.dart + - test/_data/** + - example/** + - examples/** + - build/** + - .dart_tool/** + + # Language settings for modern Dart features + language: + strict-casts: true + strict-inference: true + strict-raw-types: true + + errors: + # Security and runtime safety + invalid_assignment: error + missing_required_param: error + missing_return: error + dead_code: error + unreachable_from_main: error + + # Performance warnings + unused_local_variable: warning + unused_element: warning + unused_field: warning + unused_import: warning + unused_shown_name: warning + + # Null safety enforcement + receiver_of_type_never: error + null_check_on_nullable_type_parameter: error + + # Records and patterns (Dart 3.0+) + record_literal_one_positional_no_trailing_comma: error + + # Collections best practices + collection_methods_unrelated_type: error + unrelated_type_equality_checks: error + +linter: + rules: + # === SECURITY & SAFETY (2025 Critical) === + # Modern security practices + avoid_dynamic_calls: true + avoid_type_to_string: true + avoid_web_libraries_in_flutter: true + secure_pubspec_urls: true + + # Null safety enforcement + avoid_null_checks_in_equality_operators: true + null_check_on_nullable_type_parameter: true + unnecessary_null_checks: true + unnecessary_null_aware_assignments: true + unnecessary_null_aware_operator_on_extension_on_nullable: true + unnecessary_nullable_for_final_variable_declarations: true + + # === PERFORMANCE (2025 Focus) === + # Memory and performance optimization + avoid_unnecessary_containers: true + avoid_function_literals_in_foreach_calls: true + avoid_slow_async_io: true + prefer_const_constructors: true + prefer_const_constructors_in_immutables: true + prefer_const_declarations: true + prefer_const_literals_to_create_immutables: true + use_colored_box: true + use_decorated_box: true + sized_box_for_whitespace: true + sized_box_shrink_expand: true + + # Async/Future optimization + avoid_void_async: true + unawaited_futures: true + discarded_futures: true + await_only_futures: true + unnecessary_await_in_return: true + + # === MODERN DART FEATURES (3.0+) === + # Records and patterns support + use_super_parameters: true + matching_super_parameters: true + use_enums: true + + # === CODING STANDARDS === + # Imports and organization + always_use_package_imports: true + avoid_relative_lib_imports: true + depend_on_referenced_packages: true + directives_ordering: true + + # Code style consistency + always_declare_return_types: true + annotate_overrides: true + prefer_single_quotes: true + prefer_final_fields: true + prefer_final_locals: true + prefer_final_parameters: true + prefer_final_in_for_each: true + + # === FLUTTER SPECIFIC (2025) === + # Widget best practices + use_key_in_widget_constructors: true + use_full_hex_values_for_flutter_colors: true + sort_child_properties_last: true + no_logic_in_create_state: true + use_build_context_synchronously: true + + # === ERROR PREVENTION === + # Runtime error prevention + avoid_empty_else: true + avoid_returning_null_for_void: true + avoid_shadowing_type_parameters: true + avoid_types_as_parameter_names: true + control_flow_in_finally: true + empty_statements: true + exhaustive_cases: true + no_duplicate_case_values: true + throw_in_finally: true + + # Logic and flow + avoid_bool_literals_in_conditional_expressions: true + no_literal_bool_comparisons: true + prefer_conditional_assignment: true + prefer_if_null_operators: true + prefer_null_aware_operators: true + prefer_null_aware_method_calls: true + + # === READABILITY & MAINTAINABILITY === + # Code organization + library_names: true + library_prefixes: true + file_names: true + package_names: true + constant_identifier_names: true + non_constant_identifier_names: true + + # Documentation and comments + slash_for_doc_comments: true + comment_references: true + flutter_style_todos: true + + # Code structure + curly_braces_in_flow_control_structures: true + empty_constructor_bodies: true + prefer_collection_literals: true + prefer_contains: true + prefer_expression_function_bodies: true + prefer_foreach: true + prefer_function_declarations_over_variables: true + prefer_if_elements_to_conditional_expressions: true + prefer_initializing_formals: true + prefer_inlined_adds: true + prefer_interpolation_to_compose_strings: true + prefer_is_empty: true + prefer_is_not_empty: true + prefer_is_not_operator: true + prefer_spread_collections: true + prefer_typing_uninitialized_variables: true + + # === MODERN CLEANUP === + # Remove redundancy + unnecessary_brace_in_string_interps: true + unnecessary_const: true + unnecessary_constructor_name: true + unnecessary_getters_setters: true + unnecessary_lambdas: true + unnecessary_late: true + unnecessary_new: true + unnecessary_overrides: true + unnecessary_parenthesis: true + unnecessary_raw_strings: true + unnecessary_statements: true + unnecessary_string_escapes: true + unnecessary_string_interpolations: true + unnecessary_this: true + unnecessary_to_list_in_spreads: true + + # === PROFESSIONAL STANDARDS === + # Trailing commas for better diffs + require_trailing_commas: true + + # Parameter organization + avoid_positional_boolean_parameters: true + avoid_unused_constructor_parameters: true + + # Type safety + avoid_equals_and_hash_code_on_mutable_classes: true + hash_and_equals: true + test_types_in_equals: true + + # === DISABLED RULES (with reasons) === + # Disabled: Conflicting with our style + # always_specify_types: false # We prefer type inference where clear + # public_member_api_docs: false # Internal packages don't need full docs + # avoid_print: false # Useful in development (use debugPrint in production) + # lines_longer_than_80_chars: false # We use 120 chars + # sort_constructors_first: false # We prefer logical grouping diff --git a/example/lib/adapters_example.dart b/example/lib/adapters_example.dart index d8bcaa6..ebd840a 100644 --- a/example/lib/adapters_example.dart +++ b/example/lib/adapters_example.dart @@ -1,3 +1,4 @@ +import 'dart:async'; import 'dart:io'; import 'package:engine_tracking/engine_tracking.dart'; @@ -11,13 +12,11 @@ class AdaptersExampleApp extends StatelessWidget { const AdaptersExampleApp({super.key}); @override - Widget build(BuildContext context) { - return MaterialApp( - title: 'Engine Tracking Adapters Demo', - theme: ThemeData(primarySwatch: Colors.purple), - home: const AdaptersExamplePage(title: 'Adapters Pattern Demo'), - ); - } + Widget build(final BuildContext context) => MaterialApp( + title: 'Engine Tracking Adapters Demo', + theme: ThemeData(primarySwatch: Colors.purple), + home: const AdaptersExamplePage(title: 'Adapters Pattern Demo'), + ); } class AdaptersExamplePage extends StatefulWidget { @@ -38,7 +37,7 @@ class _AdaptersExamplePageState extends State { @override void initState() { super.initState(); - _initializeServices(); + unawaited(_initializeServices()); } Future _initializeServices() async { @@ -46,7 +45,6 @@ class _AdaptersExamplePageState extends State { await _initializeBugTracking(); } - // ConfiguraƧƵes compartilhadas - Boa prĆ”tica de reaproveitamento static final _sharedFaroConfig = EngineFaroConfig( enabled: true, endpoint: 'https://faro-collector-prod-sa-east-1.grafana.net/collect', @@ -58,9 +56,9 @@ class _AdaptersExamplePageState extends State { platform: Platform.isAndroid ? 'android' : 'ios', ); - static const _firebaseAnalyticsConfig = EngineFirebaseAnalyticsConfig(enabled: false); - static const _crashlyticsConfig = EngineCrashlyticsConfig(enabled: false); - static const _splunkConfig = EngineSplunkConfig( + static final _firebaseAnalyticsConfig = EngineFirebaseAnalyticsConfig(enabled: false); + static final _crashlyticsConfig = EngineCrashlyticsConfig(enabled: false); + static final _splunkConfig = EngineSplunkConfig( enabled: false, endpoint: 'https://splunk.example.com:8088/services/collector', token: 'demo-token', @@ -71,10 +69,9 @@ class _AdaptersExamplePageState extends State { Future _initializeAnalytics() async { try { - // Exemplo 1: Inicialização direta com adapters - final analyticsAdapters = [ + final analyticsAdapters = [ EngineFirebaseAnalyticsAdapter(_firebaseAnalyticsConfig), - EngineFaroAnalyticsAdapter(_sharedFaroConfig), // Config compartilhada + EngineFaroAnalyticsAdapter(_sharedFaroConfig), EngineSplunkAnalyticsAdapter(_splunkConfig), ]; @@ -95,10 +92,9 @@ class _AdaptersExamplePageState extends State { Future _initializeBugTracking() async { try { - // Exemplo 2: Inicialização com modelo tradicional reutilizando configs final bugTrackingModel = EngineBugTrackingModel( crashlyticsConfig: _crashlyticsConfig, - faroConfig: _sharedFaroConfig, // Mesma config do analytics! + faroConfig: _sharedFaroConfig, ); await EngineBugTracking.initWithModel(bugTrackingModel); @@ -117,204 +113,191 @@ class _AdaptersExamplePageState extends State { } @override - Widget build(BuildContext context) { - return Scaffold( - appBar: AppBar(title: Text(widget.title), backgroundColor: Theme.of(context).colorScheme.inversePrimary), - body: SingleChildScrollView( - padding: const EdgeInsets.all(16), - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - const Text( - 'šŸŽÆ Demo do PadrĆ£o Adapter', - style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold), - textAlign: TextAlign.center, - ), - const SizedBox(height: 20), - - // Analytics Status - _buildStatusCard( - title: 'šŸ“Š Analytics Adapters', - status: _analyticsStatus, - isInitialized: _analyticsInitialized, - adapters: ['Firebase Analytics Adapter', 'Grafana Faro Analytics Adapter', 'Splunk Analytics Adapter'], - ), - - const SizedBox(height: 16), - - // Bug Tracking Status - _buildStatusCard( - title: 'šŸ› Bug Tracking Adapters', - status: _bugTrackingStatus, - isInitialized: _bugTrackingInitialized, - adapters: ['Firebase Crashlytics Adapter', 'Grafana Faro Bug Tracking Adapter'], - ), - - const SizedBox(height: 30), - - const Text('šŸš€ Teste as Funcionalidades:', style: TextStyle(fontSize: 18, fontWeight: FontWeight.w600)), - const SizedBox(height: 16), - - // Analytics Actions - _buildActionSection( - title: 'Analytics Actions', - actions: [ - _buildActionButton( - 'Enviar Evento Personalizado', - Icons.analytics, - Colors.blue, - _includeInAnalyticsEvent, - ), - _buildActionButton('Definir Propriedade do UsuĆ”rio', Icons.person_add, Colors.green, _setUserProperty), - _buildActionButton('Rastrear Tela', Icons.pageview, Colors.orange, _trackScreen), - ], - ), - - const SizedBox(height: 20), - - // Bug Tracking Actions - _buildActionSection( - title: 'Bug Tracking Actions', - actions: [ - _buildActionButton('Log de Informação', Icons.info, Colors.cyan, _logInfo), - _buildActionButton('Simular Erro', Icons.error, Colors.red, _simulateError), - _buildActionButton('Definir Chave Personalizada', Icons.key, Colors.purple, _setCustomKey), - ], - ), - - const SizedBox(height: 30), - - // Advantages Card - const Card( - child: Padding( - padding: EdgeInsets.all(16.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text('✨ Vantagens do PadrĆ£o Adapter', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16)), - SizedBox(height: 12), - Text( - '• Flexibilidade total na escolha de provedores\n' - '• Inicialização independente de cada adapter\n' - '• FĆ”cil adição de novos sistemas de tracking\n' - '• Código SOLID e extensĆ­vel\n' - '• Interface consistente para todos os adapters\n' - '• Reaproveitamento de configuraƧƵes (ex: Faro)\n' - '• Dois mĆ©todos de inicialização: adapters direto ou modelos', - style: TextStyle(fontSize: 14), - ), - ], - ), + Widget build(final BuildContext context) => Scaffold( + appBar: AppBar(title: Text(widget.title), backgroundColor: Theme.of(context).colorScheme.inversePrimary), + body: SingleChildScrollView( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + const Text( + 'šŸŽÆ Demo do PadrĆ£o Adapter', + style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold), + textAlign: TextAlign.center, + ), + const SizedBox(height: 20), + + _buildStatusCard( + title: 'šŸ“Š Analytics Adapters', + status: _analyticsStatus, + isInitialized: _analyticsInitialized, + adapters: ['Firebase Analytics Adapter', 'Grafana Faro Analytics Adapter', 'Splunk Analytics Adapter'], + ), + + const SizedBox(height: 16), + + _buildStatusCard( + title: 'šŸ› Bug Tracking Adapters', + status: _bugTrackingStatus, + isInitialized: _bugTrackingInitialized, + adapters: ['Firebase Crashlytics Adapter', 'Grafana Faro Bug Tracking Adapter'], + ), + + const SizedBox(height: 30), + + const Text('šŸš€ Teste as Funcionalidades:', style: TextStyle(fontSize: 18, fontWeight: FontWeight.w600)), + const SizedBox(height: 16), + + _buildActionSection( + title: 'Analytics Actions', + actions: [ + _buildActionButton( + 'Enviar Evento Personalizado', + Icons.analytics, + Colors.blue, + _includeInAnalyticsEvent, ), - ), - - const SizedBox(height: 16), - - // Config Sharing Example Card - const Card( - color: Color(0xFFF3E5F5), - child: Padding( - padding: EdgeInsets.all(16.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - 'šŸ”„ Reaproveitamento de ConfiguraƧƵes', - style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16), - ), - SizedBox(height: 12), - Text( - 'Demonstração:\n' - '• Analytics: Inicializado com adapters diretos\n' - '• Bug Tracking: Inicializado com modelo tradicional\n' - '• Ambos compartilham a mesma config do Faro\n' - '• Economia de configuração e consistĆŖncia', - style: TextStyle(fontSize: 14), - ), - ], - ), + _buildActionButton('Definir Propriedade do UsuĆ”rio', Icons.person_add, Colors.green, _setUserProperty), + _buildActionButton('Rastrear Tela', Icons.pageview, Colors.orange, _trackScreen), + ], + ), + + const SizedBox(height: 20), + + _buildActionSection( + title: 'Bug Tracking Actions', + actions: [ + _buildActionButton('Log de Informação', Icons.info, Colors.cyan, _logInfo), + _buildActionButton('Simular Erro', Icons.error, Colors.red, _simulateError), + _buildActionButton('Definir Chave Personalizada', Icons.key, Colors.purple, _setCustomKey), + ], + ), + + const SizedBox(height: 30), + + const Card( + child: Padding( + padding: EdgeInsets.all(16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text('✨ Vantagens do PadrĆ£o Adapter', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16)), + SizedBox(height: 12), + Text( + '• Flexibilidade total na escolha de provedores\n' + '• Inicialização independente de cada adapter\n' + '• FĆ”cil adição de novos sistemas de tracking\n' + '• Código SOLID e extensĆ­vel\n' + '• Interface consistente para todos os adapters\n' + '• Reaproveitamento de configuraƧƵes (ex: Faro)\n' + '• Dois mĆ©todos de inicialização: adapters direto ou modelos', + style: TextStyle(fontSize: 14), + ), + ], ), ), - ], - ), - ), - ); - } + ), - Widget _buildStatusCard({ - required String title, - required String status, - required bool isInitialized, - required List adapters, - }) { - return Card( - elevation: 2, - child: Padding( - padding: const EdgeInsets.all(16), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text(title, style: const TextStyle(fontSize: 16, fontWeight: FontWeight.bold)), - const SizedBox(height: 8), - Container( - padding: const EdgeInsets.all(12), - decoration: BoxDecoration( - color: isInitialized ? Colors.green[50] : Colors.orange[50], - border: Border.all(color: isInitialized ? Colors.green : Colors.orange), - borderRadius: BorderRadius.circular(8), - ), - child: Row( + const SizedBox(height: 16), + + const Card( + color: Color(0xFFF3E5F5), + child: Padding( + padding: EdgeInsets.all(16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, children: [ - Icon( - isInitialized ? Icons.check_circle : Icons.info, - color: isInitialized ? Colors.green : Colors.orange, + Text( + 'šŸ”„ Reaproveitamento de ConfiguraƧƵes', + style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16), + ), + SizedBox(height: 12), + Text( + 'Demonstração:\n' + '• Analytics: Inicializado com adapters diretos\n' + '• Bug Tracking: Inicializado com modelo tradicional\n' + '• Ambos compartilham a mesma config do Faro\n' + '• Economia de configuração e consistĆŖncia', + style: TextStyle(fontSize: 14), ), - const SizedBox(width: 8), - Expanded(child: Text(status, style: const TextStyle(fontSize: 14))), ], ), ), - const SizedBox(height: 12), - const Text('Adapters disponĆ­veis:', style: TextStyle(fontWeight: FontWeight.w500)), - const SizedBox(height: 4), - ...adapters.map( - (adapter) => Padding( - padding: const EdgeInsets.only(left: 16, top: 2), - child: Text('• $adapter', style: const TextStyle(fontSize: 12, color: Colors.grey)), - ), + ), + ], + ), + ), + ); + + Widget _buildStatusCard({ + required final String title, + required final String status, + required final bool isInitialized, + required final List adapters, + }) => Card( + elevation: 2, + child: Padding( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(title, style: const TextStyle(fontSize: 16, fontWeight: FontWeight.bold)), + const SizedBox(height: 8), + Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: isInitialized ? Colors.green[50] : Colors.orange[50], + border: Border.all(color: isInitialized ? Colors.green : Colors.orange), + borderRadius: BorderRadius.circular(8), ), - ], - ), + child: Row( + children: [ + Icon( + isInitialized ? Icons.check_circle : Icons.info, + color: isInitialized ? Colors.green : Colors.orange, + ), + const SizedBox(width: 8), + Expanded(child: Text(status, style: const TextStyle(fontSize: 14))), + ], + ), + ), + const SizedBox(height: 12), + const Text('Adapters disponĆ­veis:', style: TextStyle(fontWeight: FontWeight.w500)), + const SizedBox(height: 4), + ...adapters.map( + (final adapter) => Padding( + padding: const EdgeInsets.only(left: 16, top: 2), + child: Text('• $adapter', style: const TextStyle(fontSize: 12, color: Colors.grey)), + ), + ), + ], ), - ); - } + ), + ); - Widget _buildActionSection({required String title, required List actions}) { - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text(title, style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w600)), - const SizedBox(height: 12), - ...actions.map((action) => Padding(padding: const EdgeInsets.only(bottom: 8), child: action)), - ], - ); - } + Widget _buildActionSection({required final String title, required final List actions}) => Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(title, style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w600)), + const SizedBox(height: 12), + ...actions.map((final action) => Padding(padding: const EdgeInsets.only(bottom: 8), child: action)), + ], + ); - Widget _buildActionButton(String label, IconData icon, Color color, VoidCallback onPressed) { - return SizedBox( - width: double.infinity, - child: ElevatedButton.icon( - onPressed: onPressed, - icon: Icon(icon), - label: Text(label), - style: ElevatedButton.styleFrom( - backgroundColor: color, - foregroundColor: Colors.white, - padding: const EdgeInsets.symmetric(vertical: 12), + Widget _buildActionButton(final String label, final IconData icon, final Color color, final VoidCallback onPressed) => + SizedBox( + width: double.infinity, + child: ElevatedButton.icon( + onPressed: onPressed, + icon: Icon(icon), + label: Text(label), + style: ElevatedButton.styleFrom( + backgroundColor: color, + foregroundColor: Colors.white, + padding: const EdgeInsets.symmetric(vertical: 12), + ), ), - ), - ); - } + ); Future _includeInAnalyticsEvent() async { await EngineAnalytics.logEvent('adapter_demo_event', { @@ -365,35 +348,32 @@ class _AdaptersExamplePageState extends State { _showSnackBar('Chave personalizada definida!', Colors.purple); } - void _showSnackBar(String message, Color color) { + void _showSnackBar(final String message, final Color color) { ScaffoldMessenger.of( context, ).showSnackBar(SnackBar(content: Text(message), backgroundColor: color, duration: const Duration(seconds: 2))); } } -void adaptersFlexibleExample() async { - // Exemplo 1: Usando apenas Firebase Analytics - final firebaseAnalyticsAdapter = EngineFirebaseAnalyticsAdapter(const EngineFirebaseAnalyticsConfig(enabled: true)); +Future adaptersFlexibleExample() async { + final firebaseAnalyticsAdapter = EngineFirebaseAnalyticsAdapter(EngineFirebaseAnalyticsConfig(enabled: true)); await EngineAnalytics.init([firebaseAnalyticsAdapter]); - // Exemplo 2: ConfiguraƧƵes compartilhadas entre Analytics e Bug Tracking - // Esta Ć© uma prĆ”tica recomendada para reaproveitar configuraƧƵes final sharedFaroConfig = EngineFaroConfig( enabled: true, endpoint: 'https://faro-collector.grafana.net/collect', appName: 'MyApp', appVersion: '1.0.0', environment: 'production', - apiKey: 'your-shared-faro-api-key', // Mesma chave para analytics e bug tracking + apiKey: 'your-shared-faro-api-key', namespace: 'exemple', platform: Platform.isAndroid ? 'android' : 'ios', ); - const firebaseAnalyticsConfig = EngineFirebaseAnalyticsConfig(enabled: true); - const crashlyticsConfig = EngineCrashlyticsConfig(enabled: true); - const splunkConfig = EngineSplunkConfig( + final firebaseAnalyticsConfig = EngineFirebaseAnalyticsConfig(enabled: true); + final crashlyticsConfig = EngineCrashlyticsConfig(enabled: true); + final splunkConfig = EngineSplunkConfig( enabled: true, endpoint: 'https://splunk-hec.example.com:8088/services/collector', token: 'your-hec-token', @@ -402,50 +382,49 @@ void adaptersFlexibleExample() async { index: 'main', ); - // Analytics adapters usando configuraƧƵes compartilhadas - final analyticsAdapters = [ + final analyticsAdapters = [ EngineFirebaseAnalyticsAdapter(firebaseAnalyticsConfig), - EngineFaroAnalyticsAdapter(sharedFaroConfig), // Reutilizando config + EngineFaroAnalyticsAdapter(sharedFaroConfig), EngineSplunkAnalyticsAdapter(splunkConfig), ]; - // Bug tracking adapters reutilizando a mesma config do Faro - final bugTrackingAdapters = [ + final bugTrackingAdapters = [ EngineCrashlyticsAdapter(crashlyticsConfig), - EngineFaroBugTrackingAdapter(sharedFaroConfig), // Mesma config reutilizada! + EngineFaroBugTrackingAdapter(sharedFaroConfig), ]; - // Inicialização simultĆ¢nea dos serviƧos - await Future.wait([EngineAnalytics.init(analyticsAdapters), EngineBugTracking.init(bugTrackingAdapters)]); + await Future.wait([ + EngineAnalytics.init(analyticsAdapters), + EngineBugTracking.init(bugTrackingAdapters), + ]); - // Exemplo 3: Usando modelos tradicionais com configuraƧƵes compartilhadas final analyticsModel = EngineAnalyticsModel( firebaseAnalyticsConfig: firebaseAnalyticsConfig, - faroConfig: sharedFaroConfig, // Reutilizando mesma config + faroConfig: sharedFaroConfig, splunkConfig: splunkConfig, + clarityConfig: null, + googleLoggingConfig: null, ); final bugTrackingModel = EngineBugTrackingModel( crashlyticsConfig: crashlyticsConfig, - faroConfig: sharedFaroConfig, // Mesma config compartilhada + faroConfig: sharedFaroConfig, ); - // Reinicialização usando modelos (demonstra flexibilidade) await EngineAnalytics.dispose(); await EngineBugTracking.dispose(); await Future.wait([EngineAnalytics.initWithModel(analyticsModel), EngineBugTracking.initWithModel(bugTrackingModel)]); - // Exemplo de uso após inicialização await EngineAnalytics.logEvent('adapter_demo', {'demo': 'true'}); await EngineBugTracking.log('Demo app started'); } -class CustomAnalyticsAdapter implements IEngineAnalyticsAdapter { +class CustomAnalyticsAdapter implements IEngineAnalyticsAdapter { final bool _enabled; bool _isInitialized = false; - CustomAnalyticsAdapter({required bool enabled}) : _enabled = enabled; + CustomAnalyticsAdapter({required final bool enabled}) : _enabled = enabled; @override String get adapterName => 'Custom Analytics'; @@ -460,64 +439,71 @@ class CustomAnalyticsAdapter implements IEngineAnalyticsAdapter { Future initialize() async { if (!isEnabled || _isInitialized) return; - print('Inicializando Custom Analytics Adapter...'); + debugPrint('Inicializando Custom Analytics Adapter...'); _isInitialized = true; } @override Future dispose() async { - print('Finalizando Custom Analytics Adapter...'); + debugPrint('Finalizando Custom Analytics Adapter...'); _isInitialized = false; } @override - Future logEvent(String name, [Map? parameters]) async { + Future logEvent(final String name, [final Map? parameters]) async { if (!isEnabled || !_isInitialized) return; - print('Custom Analytics - Evento: $name, ParĆ¢metros: $parameters'); + debugPrint('Custom Analytics - Evento: $name, ParĆ¢metros: $parameters'); } @override - Future setUserId(String? userId, [String? email, String? name]) async { + Future setUserId(final String? userId, [final String? email, final String? name]) async { if (!isEnabled || !_isInitialized) return; - print('Custom Analytics - UsuĆ”rio: $userId, Email: $email, Nome: $name'); + debugPrint('Custom Analytics - UsuĆ”rio: $userId, Email: $email, Nome: $name'); } @override - Future setUserProperty(String name, String? value) async { + Future setUserProperty(final String name, final String? value) async { if (!isEnabled || !_isInitialized) return; - print('Custom Analytics - Propriedade: $name = $value'); + debugPrint('Custom Analytics - Propriedade: $name = $value'); } @override - Future setPage(String screenName, [String? previousScreen, Map? parameters]) async { + Future setPage( + final String screenName, [ + final String? previousScreen, + final Map? parameters, + ]) async { if (!isEnabled || !_isInitialized) return; - print('Custom Analytics - PĆ”gina: $screenName'); + debugPrint('Custom Analytics - PĆ”gina: $screenName'); } @override - Future logAppOpen([Map? parameters]) async { + Future logAppOpen([final Map? parameters]) async { if (!isEnabled || !_isInitialized) return; - print('Custom Analytics - App aberto'); + debugPrint('Custom Analytics - App aberto'); } @override Future reset() async { if (!isEnabled || !_isInitialized) return; - print('Custom Analytics - Reset'); + debugPrint('Custom Analytics - Reset'); } + + @override + IEngineConfig get config => throw UnimplementedError(); } -class CustomBugTrackingAdapter implements IEngineBugTrackingAdapter { +class CustomBugTrackingAdapter implements IEngineBugTrackingAdapter { final bool _enabled; bool _isInitialized = false; - CustomBugTrackingAdapter({required bool enabled}) : _enabled = enabled; + CustomBugTrackingAdapter({required final bool enabled}) : _enabled = enabled; @override String get adapterName => 'Custom Bug Tracking'; @@ -532,68 +518,75 @@ class CustomBugTrackingAdapter implements IEngineBugTrackingAdapter { Future initialize() async { if (!isEnabled || _isInitialized) return; - print('Inicializando Custom Bug Tracking Adapter...'); + debugPrint('Inicializando Custom Bug Tracking Adapter...'); _isInitialized = true; } @override Future dispose() async { - print('Finalizando Custom Bug Tracking Adapter...'); + debugPrint('Finalizando Custom Bug Tracking Adapter...'); _isInitialized = false; } @override - Future setCustomKey(String key, Object value) async { + Future setCustomKey(final String key, final Object value) async { if (!isEnabled || !_isInitialized) return; - print('Custom Bug Tracking - Chave personalizada: $key = $value'); + debugPrint('Custom Bug Tracking - Chave personalizada: $key = $value'); } @override - Future setUserIdentifier(String id, String email, String name) async { + Future setUserIdentifier(final String id, final String email, final String name) async { if (!isEnabled || !_isInitialized) return; - print('Custom Bug Tracking - UsuĆ”rio: $id, Email: $email, Nome: $name'); + debugPrint('Custom Bug Tracking - UsuĆ”rio: $id, Email: $email, Nome: $name'); } @override - Future log(String message, {String? level, Map? attributes, StackTrace? stackTrace}) async { + Future log( + final String message, { + final String? level, + final Map? attributes, + final StackTrace? stackTrace, + }) async { if (!isEnabled || !_isInitialized) return; - print('Custom Bug Tracking - Log: $message [Level: $level]'); + debugPrint('Custom Bug Tracking - Log: $message [Level: $level]'); } @override Future recordError( - dynamic exception, - StackTrace? stackTrace, { - String? reason, - Iterable information = const [], - bool isFatal = false, - Map? data, + final dynamic exception, + final StackTrace? stackTrace, { + final String? reason, + final Iterable information = const [], + final bool isFatal = false, + final Map? data, }) async { if (!isEnabled || !_isInitialized) return; - print('Custom Bug Tracking - Erro: $exception [Fatal: $isFatal]'); + debugPrint('Custom Bug Tracking - Erro: $exception [Fatal: $isFatal]'); } @override - Future recordFlutterError(FlutterErrorDetails errorDetails) async { + Future recordFlutterError(final FlutterErrorDetails errorDetails) async { if (!isEnabled || !_isInitialized) return; - print('Custom Bug Tracking - Flutter Error: ${errorDetails.exception}'); + debugPrint('Custom Bug Tracking - Flutter Error: ${errorDetails.exception}'); } @override Future testCrash() async { if (!isEnabled || !_isInitialized) return; - print('Custom Bug Tracking - Test Crash'); + debugPrint('Custom Bug Tracking - Test Crash'); } + + @override + IEngineConfig get config => throw UnimplementedError(); } -void customAdaptersExample() async { - // Exemplo de adapters personalizados +Future customAdaptersExample() async { final customAnalyticsAdapter = CustomAnalyticsAdapter(enabled: true); final customBugTrackingAdapter = CustomBugTrackingAdapter(enabled: true); @@ -607,11 +600,7 @@ void customAdaptersExample() async { await EngineBugTracking.dispose(); } -void mixedInitializationExample() async { - // Exemplo mostrando diferentes mĆ©todos de inicialização - // e reaproveitamento de configuraƧƵes - - // Configuração compartilhada do Faro +Future mixedInitializationExample() async { final sharedFaroConfig = EngineFaroConfig( enabled: true, endpoint: 'https://faro.example.com/collect', @@ -623,21 +612,18 @@ void mixedInitializationExample() async { platform: Platform.isAndroid ? 'android' : 'ios', ); - // MĆ©todo 1: Analytics com adapters diretos await EngineAnalytics.init([ - EngineFirebaseAnalyticsAdapter(const EngineFirebaseAnalyticsConfig(enabled: true)), - EngineFaroAnalyticsAdapter(sharedFaroConfig), // Config compartilhada + EngineFirebaseAnalyticsAdapter(EngineFirebaseAnalyticsConfig(enabled: true)), + EngineFaroAnalyticsAdapter(sharedFaroConfig), ]); - // MĆ©todo 2: Bug tracking com modelo tradicional reutilizando config final bugTrackingModel = EngineBugTrackingModel( - crashlyticsConfig: const EngineCrashlyticsConfig(enabled: true), - faroConfig: sharedFaroConfig, // Mesma config reutilizada! + crashlyticsConfig: EngineCrashlyticsConfig(enabled: true), + faroConfig: sharedFaroConfig, ); await EngineBugTracking.initWithModel(bugTrackingModel); - // Uso demonstrando que ambos estĆ£o funcionando await EngineAnalytics.logEvent('mixed_init_demo', {'method': 'adapters'}); await EngineBugTracking.log('Mixed initialization demo', attributes: {'method': 'model'}); } diff --git a/example/lib/analytics_example.dart b/example/lib/analytics_example.dart index cea7bcf..cf1c01d 100644 --- a/example/lib/analytics_example.dart +++ b/example/lib/analytics_example.dart @@ -1,54 +1,74 @@ import 'package:engine_tracking/engine_tracking.dart'; +import 'package:flutter/material.dart'; + +Future initializeAnalyticsExample() async { + final firebaseConfig = EngineFirebaseAnalyticsConfig(enabled: true); + final faroConfig = EngineFaroConfig( + enabled: true, + endpoint: 'https://faro-collector-prod-sa-east-1.grafana.net/collect/d447b8aa9e6c8fdae9cf8c28701ede4e', + appName: 'stmr', + appVersion: '1.0.0', + environment: 'production', + apiKey: 'd447b8aa9e6c8fdae9cf8c28701ede4e', + namespace: 'analytics', + platform: 'flutter', + ); -void analyticsFlexibleExample() async { - // Exemplo 1: Usando apenas Firebase Analytics - final firebaseAdapter = EngineFirebaseAnalyticsAdapter(const EngineFirebaseAnalyticsConfig(enabled: true)); - - await EngineAnalytics.init([firebaseAdapter]); - - // Exemplo 2: Usando mĆŗltiplos adapters - final adapters = [ - EngineFirebaseAnalyticsAdapter(const EngineFirebaseAnalyticsConfig(enabled: true)), - EngineFaroAnalyticsAdapter( - const EngineFaroConfig( - enabled: true, - endpoint: 'https://faro-collector.grafana.net/collect', - appName: 'MyApp', - appVersion: '1.0.0', - environment: 'production', - apiKey: 'your-api-key', - namespace: '', - platform: '', - ), + final analyticsModel = EngineAnalyticsModel( + firebaseAnalyticsConfig: firebaseConfig, + faroConfig: faroConfig, + splunkConfig: EngineSplunkConfig( + enabled: false, + endpoint: '', + token: '', + source: '', + sourcetype: '', + index: '', ), - EngineSplunkAnalyticsAdapter( - const EngineSplunkConfig( - enabled: true, - endpoint: 'https://splunk-hec.example.com:8088/services/collector', - token: 'your-hec-token', - source: 'mobile-app', - sourcetype: 'json', - index: 'main', - ), + clarityConfig: EngineClarityConfig(enabled: false, projectId: ''), + googleLoggingConfig: EngineGoogleLoggingConfig( + enabled: false, + projectId: '', + logName: '', + credentials: {}, ), - ]; + ); - await EngineAnalytics.init(adapters); + await EngineAnalytics.initWithModel(analyticsModel); +} - // Exemplo 3: Usando o modelo tradicional (compatibilidade) - final model = EngineAnalyticsModel( - firebaseAnalyticsConfig: const EngineFirebaseAnalyticsConfig(enabled: true), - faroConfig: const EngineFaroConfig( +Future initializeAnalyticsWithMultipleAdapters() async { + final firebaseAdapter = EngineFirebaseAnalyticsAdapter(EngineFirebaseAnalyticsConfig(enabled: true)); + final faroAdapter = EngineFaroAnalyticsAdapter( + EngineFaroConfig( enabled: true, - endpoint: 'https://faro-collector.grafana.net/collect', - appName: 'MyApp', + endpoint: 'https://faro-collector-prod-sa-east-1.grafana.net/collect/d447b8aa9e6c8fdae9cf8c28701ede4e', + appName: 'stmr', appVersion: '1.0.0', environment: 'production', - apiKey: 'your-api-key', + apiKey: 'd447b8aa9e6c8fdae9cf8c28701ede4e', + namespace: 'analytics', + platform: 'flutter', + ), + ); + + await EngineAnalytics.init([firebaseAdapter, faroAdapter]); +} + +Future initializeAnalyticsWithTraditionalModel() async { + final analyticsModel = EngineAnalyticsModel( + firebaseAnalyticsConfig: EngineFirebaseAnalyticsConfig(enabled: true), + faroConfig: EngineFaroConfig( + enabled: false, + endpoint: '', + appName: '', + appVersion: '', + environment: '', + apiKey: '', namespace: '', platform: '', ), - splunkConfig: const EngineSplunkConfig( + splunkConfig: EngineSplunkConfig( enabled: false, endpoint: '', token: '', @@ -56,123 +76,100 @@ void analyticsFlexibleExample() async { sourcetype: '', index: '', ), + clarityConfig: EngineClarityConfig(enabled: false, projectId: ''), + googleLoggingConfig: EngineGoogleLoggingConfig( + enabled: false, + projectId: '', + logName: '', + credentials: {}, + ), ); - await EngineAnalytics.initWithModel(model); - - // Verificar status - print('Analytics habilitado: ${EngineAnalytics.isEnabled}'); - print('Analytics inicializado: ${EngineAnalytics.isInitialized}'); + await EngineAnalytics.initWithModel(analyticsModel); +} - // Usar normalmente - todos os providers ativos receberĆ£o os eventos - await EngineAnalytics.logEvent('user_action', {'type': 'button_click'}); - await EngineAnalytics.setUserId('user123', 'user@example.com', 'JoĆ£o Silva'); +Future demonstrateAnalyticsUsage() async { + await EngineAnalytics.setUserId('user123', 'user@example.com', 'John Doe'); - // Eventos customizados seguindo padrĆ£o Firebase - await EngineAnalytics.logEvent('purchase', { - 'transaction_id': 'txn_123', - 'currency': 'BRL', - 'value': 99.99, - 'items': [ - {'item_id': 'product_1', 'item_name': 'Product 1', 'price': 99.99}, - ], + await EngineAnalytics.logEvent('button_clicked', { + 'button_name': 'test_button', + 'screen': 'home', + 'timestamp': DateTime.now().millisecondsSinceEpoch, }); - await EngineAnalytics.logEvent('login', {'method': 'google'}); - await EngineAnalytics.logEvent('sign_up', {'method': 'email'}); + await EngineAnalytics.setUserProperty('user_level', 'premium'); + await EngineAnalytics.setUserProperty('subscription_type', 'monthly'); - // Navegação - await EngineAnalytics.setPage('home_screen', 'welcome_screen', { - 'screen_class': 'MainNavigator', - 'feature_enabled': true, + await EngineAnalytics.logEvent('screen_view', { + 'screen_name': 'HomePage', + 'screen_class': 'HomePage', }); - // App lifecycle - await EngineAnalytics.logAppOpen({'campaign': 'push_notification'}); + await EngineAnalytics.logAppOpen(); - // Limpar recursos - await EngineAnalytics.dispose(); + await EngineAnalytics.logEvent('purchase', { + 'currency': 'USD', + 'value': 29.99, + 'items': [ + {'item_id': 'premium_subscription', 'item_name': 'Premium Subscription'}, + ], + }); } class CustomAnalyticsAdapter implements IEngineAnalyticsAdapter { - final bool _enabled; bool _isInitialized = false; - CustomAnalyticsAdapter({required bool enabled}) : _enabled = enabled; - @override String get adapterName => 'Custom Analytics'; @override - bool get isEnabled => _enabled; + bool get isEnabled => true; @override bool get isInitialized => _isInitialized; @override Future initialize() async { - if (!isEnabled || _isInitialized) return; - - print('Inicializando Custom Analytics Adapter...'); _isInitialized = true; } @override Future dispose() async { - print('Finalizando Custom Analytics Adapter...'); _isInitialized = false; } @override - Future logEvent(String name, [Map? parameters]) async { - if (!isEnabled || !_isInitialized) return; - - print('Custom Analytics - Evento: $name, ParĆ¢metros: $parameters'); - } + Future reset() async {} @override - Future setUserId(String? userId, [String? email, String? name]) async { - if (!isEnabled || !_isInitialized) return; - - print('Custom Analytics - UsuĆ”rio: $userId, Email: $email, Nome: $name'); + Future logEvent(final String eventName, [final Map? parameters]) async { + debugPrint('Custom Analytics: $eventName with $parameters'); } @override - Future setUserProperty(String name, String? value) async { - if (!isEnabled || !_isInitialized) return; - - print('Custom Analytics - Propriedade: $name = $value'); + Future setUserId(final String? userId, [final String? email, final String? name]) async { + debugPrint('Custom Analytics: Set user $userId ($email)'); } @override - Future setPage(String screenName, [String? previousScreen, Map? parameters]) async { - if (!isEnabled || !_isInitialized) return; - - print('Custom Analytics - PĆ”gina: $screenName'); + Future setUserProperty(final String name, final String? value) async { + debugPrint('Custom Analytics: Set property $name = $value'); } @override - Future logAppOpen([Map? parameters]) async { - if (!isEnabled || !_isInitialized) return; - - print('Custom Analytics - App aberto'); + Future logAppOpen([final Map? parameters]) async { + debugPrint('Custom Analytics: App opened'); } @override - Future reset() async { - if (!isEnabled || !_isInitialized) return; - - print('Custom Analytics - Reset'); + Future setPage( + final String pageName, [ + final String? previousPageName, + final Map? parameters, + ]) async { + debugPrint('Custom Analytics: Page $pageName (from $previousPageName)'); } -} - -void customAdapterExample() async { - // Exemplo de adapter personalizado - final customAdapter = CustomAnalyticsAdapter(enabled: true); - - await EngineAnalytics.init([customAdapter]); - await EngineAnalytics.logEvent('custom_event', {'custom_param': 'value'}); - - await EngineAnalytics.dispose(); + @override + IEngineConfig get config => throw UnimplementedError(); } diff --git a/example/lib/google_logging_example.dart b/example/lib/google_logging_example.dart index 64a9e58..5cdd770 100644 --- a/example/lib/google_logging_example.dart +++ b/example/lib/google_logging_example.dart @@ -1,243 +1,340 @@ +// ignore_for_file: use_build_context_synchronously + +import 'dart:convert'; + import 'package:engine_tracking/engine_tracking.dart'; import 'package:flutter/material.dart'; -void main() async { - WidgetsFlutterBinding.ensureInitialized(); - - // Configuração do Google Cloud Logging - // VocĆŖ precisa criar uma Service Account no Google Cloud Console - // e baixar o arquivo JSON com as credenciais - const googleLoggingConfig = EngineGoogleLoggingConfig( - enabled: true, - projectId: 'seu-projeto-id', - logName: 'engine-tracking', - credentials: { - // Coloque aqui o conteĆŗdo do seu arquivo de credenciais JSON - "type": "service_account", - "project_id": "seu-projeto-id", - "private_key_id": "...", - "private_key": "-----BEGIN PRIVATE KEY-----\n...\n-----END PRIVATE KEY-----\n", - "client_email": "sua-service-account@seu-projeto-id.iam.gserviceaccount.com", - "client_id": "...", - "auth_uri": "https://accounts.google.com/o/oauth2/auth", - "token_uri": "https://oauth2.googleapis.com/token", - "auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs", - "client_x509_cert_url": "...", - }, - resource: { - 'type': 'global', - 'labels': { - 'project_id': 'seu-projeto-id', - }, - }, - ); +class GoogleLoggingExample extends StatefulWidget { + const GoogleLoggingExample({super.key}); - // Opção 1: Inicializar com adaptadores individuais - await EngineAnalytics.init([ - EngineGoogleLoggingAnalyticsAdapter(googleLoggingConfig), - ]); - - await EngineBugTracking.init([ - EngineGoogleLoggingBugTrackingAdapter(googleLoggingConfig), - ]); - - // Opção 2: Inicializar com modelos (recomendado) - // final analyticsModel = EngineAnalyticsModel( - // firebaseAnalyticsConfig: const EngineFirebaseAnalyticsConfig(enabled: false), - // faroConfig: const EngineFaroConfig( - // enabled: false, - // endpoint: '', - // appName: '', - // appVersion: '', - // environment: '', - // apiKey: '', - // namespace: '', - // platform: '', - // ), - // googleLoggingConfig: googleLoggingConfig, - // splunkConfig: const EngineSplunkConfig( - // enabled: false, - // endpoint: '', - // token: '', - // source: '', - // sourcetype: '', - // index: '', - // ), - // ); - - // final bugTrackingModel = EngineBugTrackingModel( - // crashlyticsConfig: const EngineCrashlyticsConfig(enabled: false), - // faroConfig: const EngineFaroConfig( - // enabled: false, - // endpoint: '', - // appName: '', - // appVersion: '', - // environment: '', - // apiKey: '', - // namespace: '', - // platform: '', - // ), - // googleLoggingConfig: googleLoggingConfig, - // ); - - // await EngineAnalytics.initWithModel(analyticsModel); - // await EngineBugTracking.initWithModel(bugTrackingModel); - - runApp(const MyApp()); + @override + State createState() => _GoogleLoggingExampleState(); } -class MyApp extends StatelessWidget { - const MyApp({super.key}); +class _GoogleLoggingExampleState extends State { + final TextEditingController _projectIdController = TextEditingController(); + final TextEditingController _logNameController = TextEditingController(); + final TextEditingController _credentialsController = TextEditingController(); + bool _isInitialized = false; + String _status = 'Not initialized'; @override - Widget build(BuildContext context) { - return MaterialApp( - title: 'Google Cloud Logging Example', - theme: ThemeData( - primarySwatch: Colors.blue, - ), - home: const GoogleLoggingExamplePage(), - ); + void initState() { + super.initState(); + _projectIdController.text = 'your-project-id'; + _logNameController.text = 'engine-tracking-logs'; + _credentialsController.text = '{}'; + } + + @override + void dispose() { + _projectIdController.dispose(); + _logNameController.dispose(); + _credentialsController.dispose(); + super.dispose(); + } + + Future _initializeGoogleLogging() async { + try { + final credentials = Map.from( + json.decode(_credentialsController.text) as Map, + ); + + final config = EngineGoogleLoggingConfig( + enabled: true, + projectId: _projectIdController.text, + logName: _logNameController.text, + credentials: credentials, + ); + + await EngineAnalytics.initWithModel( + EngineAnalyticsModel( + firebaseAnalyticsConfig: EngineFirebaseAnalyticsConfig(enabled: false), + faroConfig: EngineFaroConfig( + enabled: false, + endpoint: '', + appName: '', + appVersion: '', + environment: '', + apiKey: '', + namespace: '', + platform: '', + ), + splunkConfig: EngineSplunkConfig( + enabled: false, + endpoint: '', + token: '', + source: '', + sourcetype: '', + index: '', + ), + clarityConfig: EngineClarityConfig(enabled: false, projectId: ''), + googleLoggingConfig: config, + ), + ); + + await EngineBugTracking.initWithModel( + EngineBugTrackingModel( + crashlyticsConfig: EngineCrashlyticsConfig(enabled: false), + faroConfig: EngineFaroConfig( + enabled: false, + endpoint: '', + appName: '', + appVersion: '', + environment: '', + apiKey: '', + namespace: '', + platform: '', + ), + googleLoggingConfig: config, + ), + ); + + setState(() { + _isInitialized = true; + _status = 'Google Logging initialized successfully'; + }); + + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('Google Logging initialized successfully!'), + backgroundColor: Colors.green, + ), + ); + } catch (e) { + setState(() { + _status = 'Failed to initialize: $e'; + }); + + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text('Failed to initialize: $e'), + backgroundColor: Colors.red, + ), + ); + } + } + + Future _testAnalytics() async { + try { + await EngineAnalytics.logEvent('test_event', { + 'source': 'google_logging_example', + 'timestamp': DateTime.now().toIso8601String(), + }); + + await EngineAnalytics.setUserId('test_user', 'test@example.com', 'Test User'); + await EngineAnalytics.setUserProperty('test_property', 'test_value'); + + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('Analytics events sent to Google Logging!'), + backgroundColor: Colors.blue, + ), + ); + } catch (e) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text('Failed to send analytics: $e'), + backgroundColor: Colors.red, + ), + ); + } } -} -class GoogleLoggingExamplePage extends StatelessWidget { - const GoogleLoggingExamplePage({super.key}); + Future _testBugTracking() async { + try { + await EngineBugTracking.log('Test log message', level: 'info'); + await EngineBugTracking.setUserIdentifier('test_user', 'test@example.com', 'Test User'); + await EngineBugTracking.setCustomKey('test_key', 'test_value'); + + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('Bug tracking events sent to Google Logging!'), + backgroundColor: Colors.orange, + ), + ); + } catch (e) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text('Failed to send bug tracking: $e'), + backgroundColor: Colors.red, + ), + ); + } + } + + Future _simulateError() async { + try { + await EngineBugTracking.recordError( + Exception('Test error from Google Logging example'), + StackTrace.current, + reason: 'Demonstration error', + isFatal: false, + data: {'source': 'google_logging_example'}, + ); + + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('Error recorded in Google Logging!'), + backgroundColor: Colors.red, + ), + ); + } catch (e) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text('Failed to record error: $e'), + backgroundColor: Colors.red, + ), + ); + } + } @override - Widget build(BuildContext context) { - return Scaffold( - appBar: AppBar( - title: const Text('Google Cloud Logging Example'), - ), - body: Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - ElevatedButton( - onPressed: () async { - // Enviando evento de analytics - await EngineAnalytics.logEvent('button_clicked', { - 'button_name': 'test_analytics', - 'screen': 'home', - }); - - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar(content: Text('Analytics event sent!')), - ); - }, - child: const Text('Send Analytics Event'), - ), - const SizedBox(height: 20), - ElevatedButton( - onPressed: () async { - // Definindo identificador do usuĆ”rio - await EngineAnalytics.setUserId( - 'user123', - 'user@example.com', - 'John Doe', - ); - - await EngineBugTracking.setUserIdentifier( - 'user123', - 'user@example.com', - 'John Doe', - ); - - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar(content: Text('User identified!')), - ); - }, - child: const Text('Set User ID'), - ), - const SizedBox(height: 20), - ElevatedButton( - onPressed: () async { - // Registrando uma propriedade do usuĆ”rio - await EngineAnalytics.setUserProperty('plan', 'premium'); - - // Registrando uma chave customizada para bug tracking - await EngineBugTracking.setCustomKey('app_version', '1.0.0'); - - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar(content: Text('Properties set!')), - ); - }, - child: const Text('Set User Properties'), + Widget build(final BuildContext context) => Scaffold( + appBar: AppBar( + title: const Text('Google Logging Example'), + backgroundColor: Colors.blue, + foregroundColor: Colors.white, + ), + body: Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Card( + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Configuration', + style: Theme.of(context).textTheme.titleMedium, + ), + const SizedBox(height: 16), + TextField( + controller: _projectIdController, + decoration: const InputDecoration( + labelText: 'Project ID', + border: OutlineInputBorder(), + ), + ), + const SizedBox(height: 16), + TextField( + controller: _logNameController, + decoration: const InputDecoration( + labelText: 'Log Name', + border: OutlineInputBorder(), + ), + ), + const SizedBox(height: 16), + TextField( + controller: _credentialsController, + maxLines: 5, + decoration: const InputDecoration( + labelText: 'Service Account Credentials (JSON)', + border: OutlineInputBorder(), + helperText: 'Paste your service account JSON credentials here', + ), + ), + ], + ), ), - const SizedBox(height: 20), - ElevatedButton( - onPressed: () async { - // Registrando visualização de tela - await EngineAnalytics.setPage( - 'home_screen', - 'splash_screen', - {'campaign': 'summer_sale'}, - ); - - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar(content: Text('Page view tracked!')), - ); - }, - child: const Text('Track Page View'), - ), - const SizedBox(height: 20), - ElevatedButton( - onPressed: () async { - // Enviando log de bug tracking - await EngineBugTracking.log( - 'User performed action', - level: 'INFO', - attributes: { - 'action': 'button_click', - 'timestamp': DateTime.now().toIso8601String(), - }, - ); - - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar(content: Text('Log sent!')), - ); - }, - child: const Text('Send Log'), - ), - const SizedBox(height: 20), - ElevatedButton( - onPressed: () async { - try { - // Simulando um erro - throw Exception('Test exception for Google Cloud Logging'); - } catch (e, stackTrace) { - await EngineBugTracking.recordError( - e, - stackTrace, - reason: 'Test error button clicked', - information: ['User clicked the test error button'], - isFatal: false, - data: {'button': 'test_error'}, - ); - - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar(content: Text('Error recorded!')), - ); - } - }, - child: const Text('Test Error'), + ), + + const SizedBox(height: 16), + + ElevatedButton.icon( + onPressed: _isInitialized ? null : _initializeGoogleLogging, + icon: const Icon(Icons.cloud_upload), + label: const Text('Initialize Google Logging'), + ), + + const SizedBox(height: 16), + + Card( + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Icon( + _isInitialized ? Icons.check_circle : Icons.cancel, + color: _isInitialized ? Colors.green : Colors.red, + size: 20, + ), + const SizedBox(width: 8), + Text( + 'Status: $_status', + style: TextStyle( + color: _isInitialized ? Colors.green : Colors.red, + fontWeight: FontWeight.bold, + ), + ), + ], + ), + ], + ), ), - const SizedBox(height: 20), - ElevatedButton( - onPressed: () async { - // Testando crash (nĆ£o fatal) - await EngineBugTracking.testCrash(); - - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar(content: Text('Test crash sent!')), - ); - }, - child: const Text('Test Crash'), + ), + + const SizedBox(height: 16), + + Wrap( + spacing: 8, + runSpacing: 8, + children: [ + ElevatedButton.icon( + onPressed: _isInitialized ? _testAnalytics : null, + icon: const Icon(Icons.analytics), + label: const Text('Test Analytics'), + ), + ElevatedButton.icon( + onPressed: _isInitialized ? _testBugTracking : null, + icon: const Icon(Icons.bug_report), + label: const Text('Test Bug Tracking'), + ), + ElevatedButton.icon( + onPressed: _isInitialized ? _simulateError : null, + icon: const Icon(Icons.error), + label: const Text('Simulate Error'), + ), + ], + ), + + const SizedBox(height: 16), + + const Card( + child: Padding( + padding: EdgeInsets.all(16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Instructions', + style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16), + ), + SizedBox(height: 8), + Text( + '1. Create a Google Cloud project\n' + '2. Enable Cloud Logging API\n' + '3. Create a service account\n' + '4. Download the JSON credentials\n' + '5. Paste the credentials in the field above\n' + '6. Enter your project ID and log name\n' + '7. Click "Initialize Google Logging"\n' + '8. Test the functionality', + style: TextStyle(fontSize: 14), + ), + ], + ), ), - ], - ), + ), + ], ), - ); - } + ), + ); } diff --git a/example/lib/http_tracking_example.dart b/example/lib/http_tracking_example.dart index 623522c..b0ade61 100644 --- a/example/lib/http_tracking_example.dart +++ b/example/lib/http_tracking_example.dart @@ -1,933 +1,840 @@ +// ignore_for_file: use_build_context_synchronously + +import 'dart:async'; import 'dart:convert'; import 'package:engine_tracking/engine_tracking.dart'; import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; import 'package:http/http.dart' as http; -class HttpTrackingExample extends StatelessWidget { - const HttpTrackingExample({super.key}); - - @override - Widget build(BuildContext context) { - return MaterialApp( - title: 'HTTP Tracking Example', - theme: ThemeData(primarySwatch: Colors.blue), - home: HttpMainPage(), - routes: { - '/pokemon': (context) => PokemonListPage(), - '/posts': (context) => PostsListPage(), - '/users': (context) => UsersListPage(), - }, - ); - } +class LogEntry { + final String message; + final DateTime timestamp; + final LogType type; + final Map? data; + + LogEntry({ + required this.message, + required this.timestamp, + required this.type, + this.data, + }); + + String get formattedTime => + '${timestamp.hour.toString().padLeft(2, '0')}:' + '${timestamp.minute.toString().padLeft(2, '0')}:' + '${timestamp.second.toString().padLeft(2, '0')}'; } -class HttpMainPage extends EngineStatelessWidget { - HttpMainPage({super.key}); +enum LogType { info, success, warning, error } - @override - String get screenName => 'HttpMainPage'; +class HttpTrackingExample extends StatefulWidget { + const HttpTrackingExample({super.key}); @override - Map? get screenParameters => {'app_version': '1.0.0', 'example_type': 'http_tracking'}; + State createState() => _HttpTrackingExampleState(); +} + +class _HttpTrackingExampleState extends State { + String _status = 'Ready'; + final List _logs = []; + bool _isLoading = false; @override - Widget buildWithTracking(BuildContext context) { - return Scaffold( - appBar: AppBar( - title: const Text('Exemplos HTTP Tracking'), - backgroundColor: Colors.blue, - foregroundColor: Colors.white, - ), - body: SingleChildScrollView( - padding: const EdgeInsets.all(16.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - const Text( - 'Exemplos de Tracking com Chamadas HTTPS', - style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold), - textAlign: TextAlign.center, - ), - const SizedBox(height: 8), - const Text( - 'Este exemplo demonstra o tracking de requisiƧƵes HTTP usando APIs pĆŗblicas', - style: TextStyle(fontSize: 16, color: Colors.grey), - textAlign: TextAlign.center, - ), - const SizedBox(height: 32), + void initState() { + super.initState(); + unawaited(_initializeHttpTracking()); + } - Card( - elevation: 4, - child: Padding( - padding: const EdgeInsets.all(16.0), - child: Column( - children: [ - const Icon(Icons.catching_pokemon, size: 48, color: Colors.red), - const SizedBox(height: 8), - const Text('PokĆ©API', style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold)), - const Text('Lista de PokĆ©mons com requisiƧƵes GET', style: TextStyle(color: Colors.grey)), - const SizedBox(height: 16), - ElevatedButton( - onPressed: () { - logUserAction( - 'navigate_to_pokemon_api', - parameters: { - 'api_name': 'pokeapi', - 'navigation_method': 'button_tap', - 'source_screen': screenName, - }, - ); - Navigator.pushNamed(context, '/pokemon'); - }, - child: const Text('Ver PokĆ©mons'), - ), - ], - ), - ), - ), + Future _initializeHttpTracking() async { + try { + if (!mounted) return; - const SizedBox(height: 16), + _addLog('šŸ”„ Initializing HTTP tracking...', LogType.info); - Card( - elevation: 4, - child: Padding( - padding: const EdgeInsets.all(16.0), - child: Column( - children: [ - const Icon(Icons.article, size: 48, color: Colors.green), - const SizedBox(height: 8), - const Text('JSONPlaceholder - Posts', style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold)), - const Text('Lista de posts com GET e criação com POST', style: TextStyle(color: Colors.grey)), - const SizedBox(height: 16), - ElevatedButton( - onPressed: () { - logUserAction( - 'navigate_to_posts_api', - parameters: { - 'api_name': 'jsonplaceholder_posts', - 'navigation_method': 'button_tap', - 'source_screen': screenName, - }, - ); - Navigator.pushNamed(context, '/posts'); - }, - child: const Text('Ver Posts'), - ), - ], - ), - ), - ), + setState(() { + _status = 'HTTP Tracking initialized'; + _logs.add( + LogEntry( + message: 'āœ… HTTP tracking enabled', + timestamp: DateTime.now(), + type: LogType.success, + ), + ); + }); + + _addLog('🌐 Testing network connectivity...', LogType.info); + + try { + final testResponse = await http + .get( + Uri.parse('https://httpbin.org/get'), + ) + .timeout(const Duration(seconds: 5)); + + if (testResponse.statusCode == 200) { + _addLog('āœ… Network connectivity confirmed', LogType.success); + } else { + _addLog('āš ļø Network connectivity issue: ${testResponse.statusCode}', LogType.warning); + } + } catch (e) { + _addLog('āš ļø Network connectivity test failed: $e', LogType.warning); + } + } catch (e) { + if (!mounted) return; + + setState(() { + _status = 'Failed to initialize HTTP tracking'; + _logs.add( + LogEntry( + message: 'āŒ Failed to initialize: $e', + timestamp: DateTime.now(), + type: LogType.error, + ), + ); + }); + } + } - const SizedBox(height: 16), + void _addLog(final String message, final LogType type, [final Map? data]) { + if (!mounted) return; - Card( - elevation: 4, - child: Padding( - padding: const EdgeInsets.all(16.0), - child: Column( - children: [ - const Icon(Icons.people, size: 48, color: Colors.orange), - const SizedBox(height: 8), - const Text('JSONPlaceholder - Users', style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold)), - const Text('Lista de usuĆ”rios com requisiƧƵes GET', style: TextStyle(color: Colors.grey)), - const SizedBox(height: 16), - ElevatedButton( - onPressed: () { - logUserAction( - 'navigate_to_users_api', - parameters: { - 'api_name': 'jsonplaceholder_users', - 'navigation_method': 'button_tap', - 'source_screen': screenName, - }, - ); - Navigator.pushNamed(context, '/users'); - }, - child: const Text('Ver UsuĆ”rios'), - ), - ], - ), - ), - ), - const SizedBox(height: 16), // Adiciona espaƧo extra no final - ], + setState(() { + _logs.add( + LogEntry( + message: message, + timestamp: DateTime.now(), + type: type, + data: data, ), - ), - ); + ); + }); } -} -class PokemonListPage extends EngineStatelessWidget { - PokemonListPage({super.key}); + Future _makeGetRequest() async { + if (_isLoading) return; - @override - String get screenName => 'PokemonListPage'; + if (!mounted) return; - @override - Map? get screenParameters => {'api_name': 'pokeapi', 'endpoint': 'pokemon', 'request_type': 'GET'}; + setState(() { + _isLoading = true; + _status = 'Making GET request...'; + }); - Future> _fetchPokemonList() async { - final stopwatch = Stopwatch()..start(); + _addLog('šŸ”„ Starting GET request to JSONPlaceholder', LogType.info); try { - logCustomEvent( - 'api_request_started', - parameters: { - 'api_name': 'pokeapi', - 'endpoint': 'https://pokeapi.co/api/v2/pokemon', - 'method': 'GET', - 'timestamp': DateTime.now().toIso8601String(), - }, - ); - - final response = await http.get( - Uri.parse('https://pokeapi.co/api/v2/pokemon?limit=20'), - headers: {'Accept': 'application/json'}, - ); + final stopwatch = Stopwatch()..start(); + + final response = await http + .get( + Uri.parse('https://httpbin.org/get'), + headers: { + 'Content-Type': 'application/json', + 'User-Agent': 'EngineTracking-Example/1.0', + 'X-Request-ID': DateTime.now().millisecondsSinceEpoch.toString(), + }, + ) + .timeout(const Duration(seconds: 10)); stopwatch.stop(); - logCustomEvent( - 'api_request_completed', - parameters: { - 'api_name': 'pokeapi', - 'endpoint': 'https://pokeapi.co/api/v2/pokemon', - 'method': 'GET', - 'status_code': response.statusCode, - 'response_time_ms': stopwatch.elapsedMilliseconds, - 'response_size_bytes': response.body.length, - 'success': response.statusCode == 200, - }, - ); - if (response.statusCode == 200) { - final data = json.decode(response.body); - return data['results'] ?? []; + final data = json.decode(response.body) as Map; + if (mounted) { + setState(() { + _status = 'GET request successful'; + }); + } + _addLog( + 'āœ… GET request completed: ${response.statusCode}', + LogType.success, + { + 'duration_ms': stopwatch.elapsedMilliseconds, + 'content_length': response.contentLength, + 'response_headers': response.headers.length, + }, + ); + _addLog('šŸ“„ Response origin: ${data['origin']}', LogType.info); } else { - throw Exception('Failed to load pokemon: ${response.statusCode}'); + if (mounted) { + setState(() { + _status = 'GET request failed'; + }); + } + _addLog('āŒ GET request failed: ${response.statusCode}', LogType.error, { + 'status_code': response.statusCode, + 'reason_phrase': response.reasonPhrase, + }); + } + } on TimeoutException { + if (mounted) { + setState(() { + _status = 'GET request timeout'; + }); + } + _addLog('ā° GET request timeout after 10 seconds', LogType.error); + } catch (e) { + if (mounted) { + setState(() { + _status = 'GET request error'; + }); + } + _addLog('šŸ’„ GET request error: $e', LogType.error); + } finally { + if (mounted) { + setState(() { + _isLoading = false; + }); } - } catch (e, stackTrace) { - stopwatch.stop(); - - logScreenError( - 'Erro ao carregar lista de pokĆ©mons', - exception: e, - stackTrace: stackTrace, - additionalData: { - 'api_name': 'pokeapi', - 'endpoint': 'https://pokeapi.co/api/v2/pokemon', - 'method': 'GET', - 'response_time_ms': stopwatch.elapsedMilliseconds, - }, - ); - - rethrow; } } - @override - Widget buildWithTracking(BuildContext context) { - return Scaffold( - appBar: AppBar( - title: const Text('Lista de PokĆ©mons'), - backgroundColor: Colors.red, - foregroundColor: Colors.white, - ), - body: FutureBuilder>( - future: _fetchPokemonList(), - builder: (context, snapshot) { - if (snapshot.connectionState == ConnectionState.waiting) { - return const Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [CircularProgressIndicator(), SizedBox(height: 16), Text('Carregando pokĆ©mons...')], - ), - ); - } + Future _makePostRequest() async { + if (_isLoading) return; - if (snapshot.hasError) { - return Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - const Icon(Icons.error, size: 64, color: Colors.red), - const SizedBox(height: 16), - Text('Erro: ${snapshot.error}'), - const SizedBox(height: 16), - ElevatedButton( - onPressed: () { - logUserAction('retry_pokemon_list', parameters: {'error_message': snapshot.error.toString()}); - Navigator.pushReplacement(context, MaterialPageRoute(builder: (context) => PokemonListPage())); - }, - child: const Text('Tentar Novamente'), - ), - ], - ), - ); - } - - final pokemonList = snapshot.data ?? []; - - return ListView.builder( - itemCount: pokemonList.length, - itemBuilder: (context, index) { - final pokemon = pokemonList[index]; - return Card( - margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), - child: ListTile( - leading: const CircleAvatar(child: Icon(Icons.catching_pokemon)), - title: Text( - pokemon['name'].toString().toUpperCase(), - style: const TextStyle(fontWeight: FontWeight.bold), - ), - subtitle: Text('URL: ${pokemon['url']}'), - trailing: const Icon(Icons.arrow_forward_ios), - onTap: () { - logUserAction( - 'pokemon_item_tapped', - parameters: { - 'pokemon_name': pokemon['name'], - 'pokemon_url': pokemon['url'], - 'list_position': index, - }, - ); - - // Mostrar detalhes do pokĆ©mon - showDialog( - context: context, - builder: (context) => PokemonDetailDialog(pokemonUrl: pokemon['url']), - ); - }, - ), - ); - }, - ); - }, - ), - ); - } -} + if (!mounted) return; -/// Dialog para mostrar detalhes do pokĆ©mon -class PokemonDetailDialog extends EngineStatelessWidget { - final String pokemonUrl; + setState(() { + _isLoading = true; + _status = 'Making POST request...'; + }); - PokemonDetailDialog({super.key, required this.pokemonUrl}); + _addLog('šŸ”„ Starting POST request to httpbin.org', LogType.info); - Future> _fetchPokemonDetails() async { - final response = await http.get(Uri.parse(pokemonUrl)); - if (response.statusCode == 200) { - return json.decode(response.body); - } else { - throw Exception('Failed to load pokemon details'); - } - } + try { + final requestBody = { + 'title': 'Engine Tracking Test', + 'body': 'This is a test post from Engine Tracking HTTP example', + 'userId': 1, + 'timestamp': DateTime.now().toIso8601String(), + }; + + final stopwatch = Stopwatch()..start(); + final response = await http + .post( + Uri.parse('https://httpbin.org/post'), + headers: { + 'Content-Type': 'application/json', + 'User-Agent': 'EngineTracking-Example/1.0', + 'X-Request-ID': DateTime.now().millisecondsSinceEpoch.toString(), + }, + body: json.encode(requestBody), + ) + .timeout(const Duration(seconds: 10)); + stopwatch.stop(); - @override - Widget buildWithTracking(BuildContext context) { - return Dialog( - child: Container( - padding: const EdgeInsets.all(16), - child: FutureBuilder>( - future: _fetchPokemonDetails(), - builder: (context, snapshot) { - if (snapshot.connectionState == ConnectionState.waiting) { - return const SizedBox(height: 200, child: Center(child: CircularProgressIndicator())); - } - - if (snapshot.hasError) { - return SizedBox(height: 200, child: Center(child: Text('Erro: ${snapshot.error}'))); - } - - final pokemon = snapshot.data!; - return Column( - mainAxisSize: MainAxisSize.min, - children: [ - Text( - pokemon['name'].toString().toUpperCase(), - style: const TextStyle(fontSize: 24, fontWeight: FontWeight.bold), - ), - const SizedBox(height: 16), - if (pokemon['sprites']?['front_default'] != null) - Image.network(pokemon['sprites']['front_default'], height: 100, width: 100), - const SizedBox(height: 16), - Text('ID: ${pokemon['id']}'), - Text('Altura: ${pokemon['height']} decĆ­metros'), - Text('Peso: ${pokemon['weight']} hectogramas'), - Text('Exp. Base: ${pokemon['base_experience']}'), - const SizedBox(height: 16), - ElevatedButton(onPressed: () => Navigator.pop(context), child: const Text('Fechar')), - ], - ); + if (response.statusCode == 200) { + final data = json.decode(response.body) as Map; + if (mounted) { + setState(() { + _status = 'POST request successful'; + }); + } + _addLog( + 'āœ… POST request completed: ${response.statusCode}', + LogType.success, + { + 'duration_ms': stopwatch.elapsedMilliseconds, + 'content_length': response.contentLength, + 'response_headers': response.headers.length, }, - ), - ), - ); + ); + _addLog('šŸ“„ Response origin: ${data['origin']}', LogType.info); + } else { + if (mounted) { + setState(() { + _status = 'POST request failed'; + }); + } + _addLog('āŒ POST request failed: ${response.statusCode}', LogType.error, { + 'status_code': response.statusCode, + 'reason_phrase': response.reasonPhrase, + }); + } + } on TimeoutException { + if (mounted) { + setState(() { + _status = 'POST request timeout'; + }); + } + _addLog('ā° POST request timeout after 10 seconds', LogType.error); + } catch (e) { + if (mounted) { + setState(() { + _status = 'POST request error'; + }); + } + _addLog('šŸ’„ POST request error: $e', LogType.error); + } finally { + if (mounted) { + setState(() { + _isLoading = false; + }); + } + } } -} -/// PĆ”gina de Posts do JSONPlaceholder - Exemplo com GET e POST -class PostsListPage extends EngineStatelessWidget { - PostsListPage({super.key}); + Future _makeErrorRequest() async { + if (_isLoading) return; - @override - String get screenName => 'posts_list_page'; + if (!mounted) return; - @override - Map? get screenParameters => { - 'api_name': 'jsonplaceholder', - 'endpoint': 'posts', - 'supported_methods': ['GET', 'POST'], - }; + setState(() { + _isLoading = true; + _status = 'Making request that will fail...'; + }); - Future> _fetchPosts() async { - final stopwatch = Stopwatch()..start(); + _addLog('šŸ”„ Starting request to error endpoint', LogType.warning); try { - logCustomEvent( - 'api_request_started', - parameters: { - 'api_name': 'jsonplaceholder', - 'endpoint': 'https://jsonplaceholder.typicode.com/posts', - 'method': 'GET', - 'timestamp': DateTime.now().toIso8601String(), + final stopwatch = Stopwatch()..start(); + final response = await http + .get( + Uri.parse('https://httpbin.org/status/404'), + headers: { + 'Content-Type': 'application/json', + 'User-Agent': 'EngineTracking-Example/1.0', + 'X-Request-ID': DateTime.now().millisecondsSinceEpoch.toString(), + }, + ) + .timeout(const Duration(seconds: 10)); + stopwatch.stop(); + + if (mounted) { + setState(() { + _status = 'Error request completed'; + }); + } + _addLog( + 'āŒ Error request completed: ${response.statusCode}', + LogType.error, + { + 'duration_ms': stopwatch.elapsedMilliseconds, + 'status_code': response.statusCode, + 'reason_phrase': response.reasonPhrase, }, ); + } on TimeoutException { + if (mounted) { + setState(() { + _status = 'Error request timeout'; + }); + } + _addLog('ā° Error request timeout after 10 seconds', LogType.error); + } catch (e) { + if (mounted) { + setState(() { + _status = 'Error request failed'; + }); + } + _addLog('šŸ’„ Error request failed: $e', LogType.error); + } finally { + if (mounted) { + setState(() { + _isLoading = false; + }); + } + } + } - final response = await http.get( - Uri.parse('https://jsonplaceholder.typicode.com/posts?_limit=10'), - headers: {'Accept': 'application/json'}, - ); + Future _testDifferentConfigs() async { + if (_isLoading) return; - stopwatch.stop(); + if (!mounted) return; - logCustomEvent( - 'api_request_completed', - parameters: { - 'api_name': 'jsonplaceholder', - 'endpoint': 'https://jsonplaceholder.typicode.com/posts', - 'method': 'GET', - 'status_code': response.statusCode, - 'response_time_ms': stopwatch.elapsedMilliseconds, - 'response_size_bytes': response.body.length, - 'success': response.statusCode == 200, + setState(() { + _isLoading = true; + _status = 'Testing different configurations...'; + }); + + _addLog('šŸ”§ Testing minimal logging config', LogType.info); + + try { + await EngineHttpTracking.withConfig( + EngineHttpTrackingConfig( + enabled: true, + enableRequestLogging: true, + enableResponseLogging: true, + enableTimingLogging: true, + enableHeaderLogging: false, + enableBodyLogging: false, + maxBodyLogLength: 500, + logName: 'HTTP_MINIMAL_TEST', + ), + () async { + await http + .get( + Uri.parse('https://httpbin.org/get'), + ) + .timeout(const Duration(seconds: 10)); }, ); - if (response.statusCode == 200) { - return json.decode(response.body); - } else { - throw Exception('Failed to load posts: ${response.statusCode}'); - } - } catch (e, stackTrace) { - stopwatch.stop(); - - logScreenError( - 'Erro ao carregar lista de posts', - exception: e, - stackTrace: stackTrace, - additionalData: { - 'api_name': 'jsonplaceholder', - 'endpoint': 'https://jsonplaceholder.typicode.com/posts', - 'method': 'GET', - 'response_time_ms': stopwatch.elapsedMilliseconds, + _addLog('āœ… Minimal logging config test completed', LogType.success); + _addLog('šŸ”§ Testing errors-only config', LogType.info); + + await EngineHttpTracking.withConfig( + EngineHttpTrackingConfig( + enabled: true, + enableRequestLogging: false, + enableResponseLogging: true, + enableTimingLogging: true, + enableHeaderLogging: false, + enableBodyLogging: false, + maxBodyLogLength: 0, + logName: 'HTTP_ERRORS_TEST', + ), + () async { + await http + .get( + Uri.parse('https://httpbin.org/get'), + ) + .timeout(const Duration(seconds: 10)); + + await http + .get( + Uri.parse('https://httpbin.org/status/500'), + ) + .timeout(const Duration(seconds: 10)); }, ); - rethrow; + if (mounted) { + setState(() { + _status = 'Configuration tests completed'; + }); + } + _addLog('āœ… Errors-only config test completed', LogType.success); + } catch (e) { + _addLog('šŸ’„ Configuration test failed: $e', LogType.error); + } finally { + if (mounted) { + setState(() { + _isLoading = false; + }); + } } } - Future> _createPost(String title, String body) async { - final stopwatch = Stopwatch()..start(); + Future _testMultipleRequests() async { + if (_isLoading) return; - try { - logCustomEvent( - 'api_request_started', - parameters: { - 'api_name': 'jsonplaceholder', - 'endpoint': 'https://jsonplaceholder.typicode.com/posts', - 'method': 'POST', - 'timestamp': DateTime.now().toIso8601String(), - 'request_data': {'title': title, 'body': body}, - }, - ); + if (!mounted) return; + + setState(() { + _isLoading = true; + _status = 'Making multiple concurrent requests...'; + }); + + _addLog('šŸ”„ Starting 5 concurrent requests', LogType.info); - final response = await http.post( - Uri.parse('https://jsonplaceholder.typicode.com/posts'), - headers: {'Content-Type': 'application/json; charset=UTF-8'}, - body: json.encode({'title': title, 'body': body, 'userId': 1}), + try { + final futures = List.generate( + 5, + (final index) async => http + .get( + Uri.parse('https://httpbin.org/delay/${index + 1}'), + headers: { + 'User-Agent': 'EngineTracking-Example/1.0', + 'X-Request-ID': 'batch_${DateTime.now().millisecondsSinceEpoch}_$index', + }, + ) + .timeout(const Duration(seconds: 15)), ); + final stopwatch = Stopwatch()..start(); + final responses = await Future.wait(futures); stopwatch.stop(); - logCustomEvent( - 'api_request_completed', - parameters: { - 'api_name': 'jsonplaceholder', - 'endpoint': 'https://jsonplaceholder.typicode.com/posts', - 'method': 'POST', - 'status_code': response.statusCode, - 'response_time_ms': stopwatch.elapsedMilliseconds, - 'response_size_bytes': response.body.length, - 'success': response.statusCode == 201, - }, - ); + final successCount = responses.where((final r) => r.statusCode == 200).length; - if (response.statusCode == 201) { - return json.decode(response.body); - } else { - throw Exception('Failed to create post: ${response.statusCode}'); + if (mounted) { + setState(() { + _status = 'Multiple requests completed'; + }); } - } catch (e, stackTrace) { - stopwatch.stop(); - logScreenError( - 'Erro ao criar post', - exception: e, - stackTrace: stackTrace, - additionalData: { - 'api_name': 'jsonplaceholder', - 'endpoint': 'https://jsonplaceholder.typicode.com/posts', - 'method': 'POST', - 'response_time_ms': stopwatch.elapsedMilliseconds, - 'request_data': {'title': title, 'body': body}, + _addLog( + 'āœ… Completed $successCount/5 requests successfully', + LogType.success, + { + 'total_duration_ms': stopwatch.elapsedMilliseconds, + 'success_rate': '${(successCount / 5 * 100).toStringAsFixed(1)}%', }, ); - - rethrow; + } catch (e) { + _addLog('šŸ’„ Multiple requests failed: $e', LogType.error); + } finally { + if (mounted) { + setState(() { + _isLoading = false; + }); + } } } - void _showCreatePostDialog(BuildContext context) { - final titleController = TextEditingController(); - final bodyController = TextEditingController(); - - showDialog( - context: context, - builder: (context) => AlertDialog( - title: const Text('Criar Novo Post'), - content: Column( - mainAxisSize: MainAxisSize.min, - children: [ - TextField( - controller: titleController, - decoration: const InputDecoration(labelText: 'TĆ­tulo', border: OutlineInputBorder()), - ), - const SizedBox(height: 16), - TextField( - controller: bodyController, - decoration: const InputDecoration(labelText: 'ConteĆŗdo', border: OutlineInputBorder()), - maxLines: 3, - ), - ], - ), - actions: [ - TextButton(onPressed: () => Navigator.pop(context), child: const Text('Cancelar')), - ElevatedButton( - onPressed: () async { - if (titleController.text.isNotEmpty && bodyController.text.isNotEmpty) { - Navigator.pop(context); - - try { - logUserAction( - 'create_post_attempted', - parameters: { - 'title_length': titleController.text.length, - 'body_length': bodyController.text.length, - }, - ); - - final newPost = await _createPost(titleController.text, bodyController.text); - - logUserAction( - 'create_post_success', - parameters: {'post_id': newPost['id'], 'title': newPost['title']}, - ); - - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text('Post criado com sucesso! ID: ${newPost['id']}'), - backgroundColor: Colors.green, - ), - ); - } catch (e) { - ScaffoldMessenger.of( - context, - ).showSnackBar(SnackBar(content: Text('Erro ao criar post: $e'), backgroundColor: Colors.red)); - } - } - }, - child: const Text('Criar'), - ), - ], - ), - ); - } - - @override - Widget buildWithTracking(BuildContext context) { - return Scaffold( - appBar: AppBar( - title: const Text('Posts - JSONPlaceholder'), - backgroundColor: Colors.green, - foregroundColor: Colors.white, - actions: [ - IconButton( - icon: const Icon(Icons.add), - onPressed: () { - logUserAction('add_post_button_tapped', parameters: {'source_screen': screenName}); - _showCreatePostDialog(context); - }, - ), - ], - ), - body: FutureBuilder>( - future: _fetchPosts(), - builder: (context, snapshot) { - if (snapshot.connectionState == ConnectionState.waiting) { - return const Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [CircularProgressIndicator(), SizedBox(height: 16), Text('Carregando posts...')], - ), - ); - } + void _clearLogs() { + if (!mounted) return; - if (snapshot.hasError) { - return Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - const Icon(Icons.error, size: 64, color: Colors.red), - const SizedBox(height: 16), - Text('Erro: ${snapshot.error}'), - const SizedBox(height: 16), - ElevatedButton( - onPressed: () { - logUserAction('retry_posts_list', parameters: {'error_message': snapshot.error.toString()}); - Navigator.pushReplacement(context, MaterialPageRoute(builder: (context) => PostsListPage())); - }, - child: const Text('Tentar Novamente'), - ), - ], - ), - ); - } - - final posts = snapshot.data ?? []; - - return ListView.builder( - itemCount: posts.length, - itemBuilder: (context, index) { - final post = posts[index]; - return Card( - margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), - child: ListTile( - leading: CircleAvatar(child: Text('${post['id']}')), - title: Text( - post['title'], - style: const TextStyle(fontWeight: FontWeight.bold), - maxLines: 2, - overflow: TextOverflow.ellipsis, - ), - subtitle: Text(post['body'], maxLines: 2, overflow: TextOverflow.ellipsis), - onTap: () { - logUserAction( - 'post_item_tapped', - parameters: {'post_id': post['id'], 'post_title': post['title'], 'list_position': index}, - ); - - showDialog( - context: context, - builder: (context) => AlertDialog( - title: Text('Post #${post['id']}'), - content: SingleChildScrollView( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisSize: MainAxisSize.min, - children: [ - Text(post['title'], style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 16)), - const SizedBox(height: 16), - Text(post['body']), - ], - ), - ), - actions: [TextButton(onPressed: () => Navigator.pop(context), child: const Text('Fechar'))], - ), - ); - }, - ), - ); - }, - ); - }, - ), - ); + setState(() { + _logs.clear(); + _status = 'Logs cleared'; + }); } -} - -/// PĆ”gina de UsuĆ”rios do JSONPlaceholder - Exemplo com GET -class UsersListPage extends EngineStatelessWidget { - UsersListPage({super.key}); - - @override - String get screenName => 'users_list_page'; - - @override - Map? get screenParameters => { - 'api_name': 'jsonplaceholder', - 'endpoint': 'users', - 'request_type': 'GET', - }; - Future> _fetchUsers() async { - final stopwatch = Stopwatch()..start(); + Future _copyLog(final LogEntry log) async { + final logText = _formatLogForCopy(log); + await Clipboard.setData(ClipboardData(text: logText)); - try { - logCustomEvent( - 'api_request_started', - parameters: { - 'api_name': 'jsonplaceholder', - 'endpoint': 'https://jsonplaceholder.typicode.com/users', - 'method': 'GET', - 'timestamp': DateTime.now().toIso8601String(), - }, + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('Log entry copied to clipboard'), + duration: Duration(seconds: 1), + ), ); + } + } - final response = await http.get( - Uri.parse('https://jsonplaceholder.typicode.com/users'), - headers: {'Accept': 'application/json'}, + Future _copyAllLogs() async { + if (_logs.isEmpty) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('No logs to copy'), + duration: Duration(seconds: 1), + ), ); + return; + } - stopwatch.stop(); + final allLogsText = _logs.map(_formatLogForCopy).join('\n\n'); + await Clipboard.setData(ClipboardData(text: allLogsText)); - logCustomEvent( - 'api_request_completed', - parameters: { - 'api_name': 'jsonplaceholder', - 'endpoint': 'https://jsonplaceholder.typicode.com/users', - 'method': 'GET', - 'status_code': response.statusCode, - 'response_time_ms': stopwatch.elapsedMilliseconds, - 'response_size_bytes': response.body.length, - 'success': response.statusCode == 200, - 'users_count': response.statusCode == 200 ? json.decode(response.body).length : 0, - }, + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text('${_logs.length} log entries copied to clipboard'), + duration: const Duration(seconds: 2), + ), ); + } + } - if (response.statusCode == 200) { - return json.decode(response.body); - } else { - throw Exception('Failed to load users: ${response.statusCode}'); - } - } catch (e, stackTrace) { - stopwatch.stop(); + String _formatLogForCopy(final LogEntry log) { + final timestamp = log.timestamp.toIso8601String(); + final type = log.type.name.toUpperCase(); + final message = log.message; - logScreenError( - 'Erro ao carregar lista de usuĆ”rios', - exception: e, - stackTrace: stackTrace, - additionalData: { - 'api_name': 'jsonplaceholder', - 'endpoint': 'https://jsonplaceholder.typicode.com/users', - 'method': 'GET', - 'response_time_ms': stopwatch.elapsedMilliseconds, - }, - ); + String result = '[$timestamp] [$type] $message'; - rethrow; + if (log.data != null && log.data!.isNotEmpty) { + result += '\nData: ${json.encode(log.data)}'; } + + return result; } - @override - Widget buildWithTracking(BuildContext context) { - return Scaffold( - appBar: AppBar( - title: const Text('UsuĆ”rios - JSONPlaceholder'), - backgroundColor: Colors.orange, - foregroundColor: Colors.white, + void _showTrackingStats() { + final stats = EngineHttpTracking.getStats(); + unawaited( + showDialog( + context: context, + builder: (final context) => AlertDialog( + title: const Text('HTTP Tracking Stats'), + content: SingleChildScrollView( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + ...stats.entries.map( + (final e) => Padding( + padding: const EdgeInsets.symmetric(vertical: 4.0), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox( + width: 120, + child: Text( + '${e.key}:', + style: const TextStyle(fontWeight: FontWeight.bold), + ), + ), + Expanded( + child: Text( + '${e.value}', + style: const TextStyle(fontFamily: 'monospace'), + ), + ), + ], + ), + ), + ), + ], + ), + ), + actions: [ + TextButton( + onPressed: () async { + await Clipboard.setData( + ClipboardData( + text: stats.entries.map((final e) => '${e.key}: ${e.value}').join('\n'), + ), + ); + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('Stats copied to clipboard')), + ); + }, + child: const Text('Copy'), + ), + TextButton( + onPressed: () => Navigator.of(context).pop(), + child: const Text('Close'), + ), + ], + ), ), - body: FutureBuilder>( - future: _fetchUsers(), - builder: (context, snapshot) { - if (snapshot.connectionState == ConnectionState.waiting) { - return const Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [CircularProgressIndicator(), SizedBox(height: 16), Text('Carregando usuĆ”rios...')], - ), - ); - } + ); + } + + Color _getLogColor(final LogType type) { + switch (type) { + case LogType.success: + return Colors.green; + case LogType.warning: + return Colors.orange; + case LogType.error: + return Colors.red; + case LogType.info: + return Colors.blue; + } + } - if (snapshot.hasError) { - return Center( + IconData _getLogIcon(final LogType type) { + switch (type) { + case LogType.success: + return Icons.check_circle; + case LogType.warning: + return Icons.warning; + case LogType.error: + return Icons.error; + case LogType.info: + return Icons.info; + } + } + + @override + Widget build(final BuildContext context) => Scaffold( + appBar: AppBar( + title: const Text('HTTP Tracking Example'), + backgroundColor: Colors.blue, + foregroundColor: Colors.white, + actions: [ + IconButton( + icon: const Icon(Icons.refresh), + onPressed: _initializeHttpTracking, + tooltip: 'Reinitialize HTTP Tracking', + ), + ], + ), + body: Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + children: [ + Card( + child: Padding( + padding: const EdgeInsets.all(16.0), child: Column( - mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.start, children: [ - const Icon(Icons.error, size: 64, color: Colors.red), - const SizedBox(height: 16), - Text('Erro: ${snapshot.error}'), - const SizedBox(height: 16), - ElevatedButton( - onPressed: () { - logUserAction('retry_users_list', parameters: {'error_message': snapshot.error.toString()}); - Navigator.pushReplacement(context, MaterialPageRoute(builder: (context) => UsersListPage())); - }, - child: const Text('Tentar Novamente'), + Row( + children: [ + Icon( + _isLoading ? Icons.hourglass_empty : Icons.check_circle, + color: _isLoading ? Colors.orange : Colors.green, + size: 20, + ), + const SizedBox(width: 8), + Expanded( + child: Text( + 'Status: $_status', + style: Theme.of(context).textTheme.titleMedium, + ), + ), + ], ), - ], - ), - ); - } - - final users = snapshot.data ?? []; - - return ListView.builder( - itemCount: users.length, - itemBuilder: (context, index) { - final user = users[index]; - return Card( - margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), - child: ListTile( - leading: CircleAvatar(child: Text(user['name'][0])), - title: Text(user['name'], style: const TextStyle(fontWeight: FontWeight.bold)), - subtitle: Column( - crossAxisAlignment: CrossAxisAlignment.start, + const SizedBox(height: 8), + Row( children: [ - Text('Email: ${user['email']}'), - Text('Telefone: ${user['phone']}'), - Text('Website: ${user['website']}'), + Icon( + EngineHttpTracking.isEnabled ? Icons.check_circle : Icons.cancel, + color: EngineHttpTracking.isEnabled ? Colors.green : Colors.red, + size: 20, + ), + const SizedBox(width: 8), + Text( + 'HTTP Tracking: ${EngineHttpTracking.isEnabled ? "Enabled" : "Disabled"}', + style: TextStyle( + color: EngineHttpTracking.isEnabled ? Colors.green : Colors.red, + fontWeight: FontWeight.bold, + ), + ), ], ), - isThreeLine: true, - onTap: () { - logUserAction( - 'user_item_tapped', - parameters: { - 'user_id': user['id'], - 'user_name': user['name'], - 'user_email': user['email'], - 'list_position': index, - }, - ); - - showDialog( - context: context, - builder: (context) => AlertDialog( - title: Text(user['name']), - content: SingleChildScrollView( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisSize: MainAxisSize.min, - children: [ - _buildUserInfoRow('ID', user['id'].toString()), - _buildUserInfoRow('Username', user['username']), - _buildUserInfoRow('Email', user['email']), - _buildUserInfoRow('Telefone', user['phone']), - _buildUserInfoRow('Website', user['website']), - const Divider(), - const Text('EndereƧo:', style: TextStyle(fontWeight: FontWeight.bold)), - _buildUserInfoRow('Rua', '${user['address']['street']}, ${user['address']['suite']}'), - _buildUserInfoRow('Cidade', user['address']['city']), - _buildUserInfoRow('CEP', user['address']['zipcode']), - const Divider(), - const Text('Empresa:', style: TextStyle(fontWeight: FontWeight.bold)), - _buildUserInfoRow('Nome', user['company']['name']), - _buildUserInfoRow('Slogan', user['company']['catchPhrase']), - ], - ), + ], + ), + ), + ), + + const SizedBox(height: 16), + + Wrap( + spacing: 8, + runSpacing: 8, + children: [ + ElevatedButton.icon( + onPressed: _isLoading ? null : _makeGetRequest, + icon: const Icon(Icons.download), + label: const Text('GET Request'), + ), + ElevatedButton.icon( + onPressed: _isLoading ? null : _makePostRequest, + icon: const Icon(Icons.upload), + label: const Text('POST Request'), + ), + ElevatedButton.icon( + onPressed: _isLoading ? null : _makeErrorRequest, + icon: const Icon(Icons.error), + label: const Text('Error Request'), + ), + ElevatedButton.icon( + onPressed: _isLoading ? null : _testMultipleRequests, + icon: const Icon(Icons.multiple_stop), + label: const Text('Multiple Requests'), + ), + ElevatedButton.icon( + onPressed: _isLoading ? null : _testDifferentConfigs, + icon: const Icon(Icons.settings), + label: const Text('Test Configs'), + ), + ElevatedButton.icon( + onPressed: _showTrackingStats, + icon: const Icon(Icons.analytics), + label: const Text('Show Stats'), + ), + ElevatedButton.icon( + onPressed: _clearLogs, + icon: const Icon(Icons.clear), + label: const Text('Clear Logs'), + ), + ElevatedButton.icon( + onPressed: _logs.isNotEmpty ? _copyAllLogs : null, + icon: const Icon(Icons.copy), + label: const Text('Copy All Logs'), + ), + ], + ), + + const SizedBox(height: 16), + + Expanded( + child: Card( + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + const Icon(Icons.list), + const SizedBox(width: 8), + Text( + 'Activity Log (${_logs.length} entries)', + style: Theme.of(context).textTheme.titleMedium, ), - actions: [TextButton(onPressed: () => Navigator.pop(context), child: const Text('Fechar'))], + ], + ), + const SizedBox(height: 8), + Expanded( + child: ListView.builder( + itemCount: _logs.length, + itemBuilder: (final context, final index) { + final log = _logs[_logs.length - 1 - index]; + return Card( + margin: const EdgeInsets.symmetric(vertical: 2.0), + child: Padding( + padding: const EdgeInsets.all(8.0), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Icon( + _getLogIcon(log.type), + color: _getLogColor(log.type), + size: 16, + ), + const SizedBox(width: 8), + Text( + log.formattedTime, + style: const TextStyle( + fontFamily: 'monospace', + fontSize: 12, + ), + ), + const SizedBox(width: 8), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + log.message, + style: const TextStyle(fontSize: 12), + ), + if (log.data != null && log.data!.isNotEmpty) + Padding( + padding: const EdgeInsets.only(top: 4.0), + child: Text( + 'Data: ${json.encode(log.data)}', + style: const TextStyle( + fontSize: 10, + fontFamily: 'monospace', + color: Colors.grey, + ), + ), + ), + ], + ), + ), + IconButton( + icon: const Icon(Icons.copy, size: 16), + onPressed: () => _copyLog(log), + tooltip: 'Copy log entry', + padding: EdgeInsets.zero, + constraints: const BoxConstraints( + minWidth: 24, + minHeight: 24, + ), + ), + ], + ), + ), + ); + }, ), - ); - }, + ), + ], ), - ); - }, - ); - }, - ), - ); - } - - Widget _buildUserInfoRow(String label, String value) { - return Padding( - padding: const EdgeInsets.symmetric(vertical: 2), - child: Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - SizedBox( - width: 80, - child: Text('$label:', style: const TextStyle(fontWeight: FontWeight.w500)), + ), + ), ), - Expanded(child: Text(value)), ], ), - ); - } -} - -/// Função para demonstrar a inicialização do sistema com tracking HTTP -Future initializeHttpTrackingExample() async { - final analyticsModel = EngineAnalyticsModel( - firebaseAnalyticsConfig: const EngineFirebaseAnalyticsConfig(enabled: false), - faroConfig: const EngineFaroConfig( - enabled: true, - endpoint: 'https://faro-collector-prod-us-east-0.grafana.net/collect', - appName: 'engine_tracking_http_example', - appVersion: '1.0.0', - environment: 'development', - apiKey: 'your-api-key-here', - namespace: 'flutter_app', - platform: 'mobile', - ), - splunkConfig: const EngineSplunkConfig( - enabled: false, - endpoint: '', - token: '', - source: '', - sourcetype: '', - index: '', - ), - clarityConfig: const EngineClarityConfig( - enabled: false, - projectId: '', - ), - googleLoggingConfig: const EngineGoogleLoggingConfig( - enabled: false, - projectId: '', - logName: '', - credentials: {}, ), ); - final bugTrackingModel = EngineBugTrackingModel( - crashlyticsConfig: const EngineCrashlyticsConfig(enabled: false), - faroConfig: const EngineFaroConfig( - enabled: true, - endpoint: 'https://faro-collector-prod-us-east-0.grafana.net/collect', - appName: 'engine_tracking_http_example', - appVersion: '1.0.0', - environment: 'development', - apiKey: 'your-api-key-here', - namespace: 'flutter_app', - platform: 'mobile', - ), - googleLoggingConfig: const EngineGoogleLoggingConfig( - enabled: false, - projectId: '', - logName: '', - credentials: {}, - ), - ); - - await EngineAnalytics.initWithModel(analyticsModel); - await EngineBugTracking.initWithModel(bugTrackingModel); - - print('Sistema de tracking HTTP inicializado com sucesso!'); - print('- PokĆ©API: https://pokeapi.co/api/v2/pokemon'); - print('- JSONPlaceholder Posts: https://jsonplaceholder.typicode.com/posts'); - print('- JSONPlaceholder Users: https://jsonplaceholder.typicode.com/users'); + @override + void dispose() { + EngineHttpTracking.disable(); + super.dispose(); + } } diff --git a/example/lib/main.dart b/example/lib/main.dart index df23286..37e265a 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -1,21 +1,16 @@ +import 'dart:async'; import 'dart:io'; import 'package:engine_tracking/engine_tracking.dart'; +import 'package:engine_tracking_example/http_tracking_example.dart'; +import 'package:engine_tracking_example/view_tracking_example.dart'; import 'package:flutter/material.dart'; -import 'view_tracking_example.dart'; -import 'http_tracking_example.dart'; - void main() async { WidgetsFlutterBinding.ensureInitialized(); - final (analyticsModel, bugTrackingModel) = await initializeTracking(); + await initializeTracking(); - runApp( - EngineWidget( - app: const MyApp(), - clarityConfig: analyticsModel.clarityConfig, - ), - ); + runApp(const EngineWidget(app: MyApp())); } Future<(EngineAnalyticsModel analyticsModel, EngineBugTrackingModel bugTrackingModel)> initializeTracking() async { @@ -28,6 +23,7 @@ Future<(EngineAnalyticsModel analyticsModel, EngineBugTrackingModel bugTrackingM apiKey: '54d9b2d4c4e2a550c890876a914a3525', namespace: 'engine.stmr.tech', platform: Platform.isAndroid ? 'android' : 'ios', + httpTrackingEnable: true, ); final clarityConfig = EngineClarityConfig( @@ -36,9 +32,11 @@ Future<(EngineAnalyticsModel analyticsModel, EngineBugTrackingModel bugTrackingM ); final analyticsModel = EngineAnalyticsModel( - firebaseAnalyticsConfig: const EngineFirebaseAnalyticsConfig(enabled: false), + firebaseAnalyticsConfig: EngineFirebaseAnalyticsConfig( + enabled: false, + ), faroConfig: faroConfig, - splunkConfig: const EngineSplunkConfig( + splunkConfig: EngineSplunkConfig( enabled: false, endpoint: '', token: '', @@ -47,7 +45,7 @@ Future<(EngineAnalyticsModel analyticsModel, EngineBugTrackingModel bugTrackingM index: '', ), clarityConfig: clarityConfig, - googleLoggingConfig: const EngineGoogleLoggingConfig( + googleLoggingConfig: EngineGoogleLoggingConfig( enabled: false, projectId: '', logName: '', @@ -56,9 +54,9 @@ Future<(EngineAnalyticsModel analyticsModel, EngineBugTrackingModel bugTrackingM ); final bugTrackingModel = EngineBugTrackingModel( - crashlyticsConfig: const EngineCrashlyticsConfig(enabled: false), + crashlyticsConfig: EngineCrashlyticsConfig(enabled: false), faroConfig: faroConfig, - googleLoggingConfig: const EngineGoogleLoggingConfig( + googleLoggingConfig: EngineGoogleLoggingConfig( enabled: false, projectId: '', logName: '', @@ -72,13 +70,36 @@ Future<(EngineAnalyticsModel analyticsModel, EngineBugTrackingModel bugTrackingM EngineBugTracking.initWithModel(bugTrackingModel), ]); + final httpTrackingConfig = EngineHttpTrackingConfig( + enabled: true, + enableRequestLogging: true, + enableResponseLogging: true, + enableTimingLogging: true, + enableHeaderLogging: true, + enableBodyLogging: true, + maxBodyLogLength: 2000, + ); + + EngineHttpTracking.initialize(httpTrackingConfig); + await Future.wait([ - EngineAnalytics.setUserId('demo_user_123', 'demo@example.com', 'Demo User'), - EngineBugTracking.setUserIdentifier('demo_user_123', 'demo@example.com', 'Demo User'), + EngineAnalytics.setUserId( + 'demo_user_123', + 'demo@example.com', + 'Demo User', + ), + EngineBugTracking.setUserIdentifier( + 'demo_user_123', + 'demo@example.com', + 'Demo User', + ), ]); await EngineAnalytics.logAppOpen(); await EngineBugTracking.log('App initialized successfully', level: 'info'); + await EngineHttpTracking.logCustomEvent( + 'HTTP tracking initialized for example app', + ); return (analyticsModel, bugTrackingModel); } catch (e, stackTrace) { @@ -97,14 +118,12 @@ class MyApp extends StatelessWidget { const MyApp({super.key}); @override - Widget build(BuildContext context) { - return MaterialApp( - title: 'Engine Tracking Example', - theme: ThemeData(primarySwatch: Colors.blue, useMaterial3: true), - home: const HomePage(), - navigatorObservers: [EngineNavigationObserver()], - ); - } + Widget build(final BuildContext context) => MaterialApp( + title: 'Engine Tracking Example', + theme: ThemeData(primarySwatch: Colors.blue, useMaterial3: true), + home: const HomePage(), + navigatorObservers: [EngineNavigationObserver()], + ); } class HomePage extends StatefulWidget { @@ -120,10 +139,10 @@ class _HomePageState extends State { @override void initState() { super.initState(); - EngineAnalytics.setPage('HomePage'); + unawaited(EngineAnalytics.setPage('HomePage')); } - void _incrementCounter() async { + Future _incrementCounter() async { setState(() { _counter++; }); @@ -141,7 +160,7 @@ class _HomePageState extends State { ); } - void _simulateError() async { + Future _simulateError() async { try { throw Exception('This is a simulated error for testing purposes'); } catch (error, stackTrace) { @@ -155,139 +174,171 @@ class _HomePageState extends State { ); if (mounted) { - ScaffoldMessenger.of( - context, - ).showSnackBar(const SnackBar(content: Text('Error recorded and logged!'), backgroundColor: Colors.orange)); + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('Error recorded and logged!'), + backgroundColor: Colors.orange, + ), + ); } } } - void _setUserProperty() async { + Future _setUserProperty() async { await EngineAnalytics.setUserProperty('user_level', 'beginner'); await EngineBugTracking.setCustomKey('user_engagement', 'active'); if (mounted) { - ScaffoldMessenger.of( - context, - ).showSnackBar(const SnackBar(content: Text('User properties updated!'), backgroundColor: Colors.green)); + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('User properties updated!'), + backgroundColor: Colors.green, + ), + ); } } - void _navigateToSecondPage() async { - await EngineAnalytics.logEvent('navigation', {'from_page': 'HomePage', 'to_page': 'SecondPage'}); - + Future _navigateToSecondPage() async { if (mounted) { - Navigator.push(context, MaterialPageRoute(builder: (context) => const SecondPage())); + unawaited( + Navigator.push( + context, + MaterialPageRoute(builder: (final context) => const SecondPage()), + ), + ); } } - void _navigateToViewTrackingExample() async { - await EngineAnalytics.logEvent('navigation', {'from_page': 'HomePage', 'to_page': 'ViewTrackingExample'}); - + Future _navigateToViewTrackingExample() async { if (mounted) { - Navigator.push(context, MaterialPageRoute(builder: (context) => const ViewTrackingExample())); + unawaited(Navigator.push(context, MaterialPageRoute(builder: (final context) => const ViewTrackingExample()))); } } - void _navigateToHttpTrackingExample() async { - await EngineAnalytics.logEvent('navigation', {'from_page': 'HomePage', 'to_page': 'HttpTrackingExample'}); - + Future _navigateToHttpTrackingExample() async { if (mounted) { - Navigator.push(context, MaterialPageRoute(builder: (context) => const HttpTrackingExample())); + unawaited( + Navigator.push( + context, + MaterialPageRoute(builder: (final context) => const HttpTrackingExample()), + ), + ); } } @override - Widget build(BuildContext context) { - return Scaffold( - appBar: AppBar( - backgroundColor: Theme.of(context).colorScheme.inversePrimary, - title: const Text('Engine Tracking Example'), - ), - body: Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - const Icon(Icons.analytics, size: 80, color: Colors.blue), - const SizedBox(height: 20), - const Text('Engine Tracking Demo', style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold)), - const SizedBox(height: 20), - Text('You have pushed the button this many times:', style: Theme.of(context).textTheme.bodyLarge), - Text('$_counter', style: Theme.of(context).textTheme.headlineMedium), - const SizedBox(height: 40), - - Card( - margin: const EdgeInsets.symmetric(horizontal: 20, vertical: 10), - child: Padding( - padding: const EdgeInsets.all(16), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const Text('Service Status:', style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)), - const SizedBox(height: 10), - _buildStatusRow('Analytics', EngineAnalytics.isEnabled), - _buildStatusRow('Bug Tracking', EngineBugTracking.isEnabled), - ], - ), + Widget build(final BuildContext context) => Scaffold( + appBar: AppBar( + backgroundColor: Theme.of(context).colorScheme.inversePrimary, + title: const Text('Engine Tracking Example'), + ), + body: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Icon(Icons.analytics, size: 80, color: Colors.blue), + const SizedBox(height: 20), + const Text( + 'Engine Tracking Demo', + style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold), + ), + const SizedBox(height: 20), + Text( + 'You have pushed the button this many times:', + style: Theme.of(context).textTheme.bodyLarge, + ), + Text( + '$_counter', + style: Theme.of(context).textTheme.headlineMedium, + ), + const SizedBox(height: 40), + + Card( + margin: const EdgeInsets.symmetric(horizontal: 20, vertical: 10), + child: Padding( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + 'Service Status:', + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(height: 10), + _buildStatusRow('Analytics', EngineAnalytics.isEnabled), + _buildStatusRow( + 'Bug Tracking', + EngineBugTracking.isEnabled, + ), + _buildStatusRow( + 'HTTP Tracking', + EngineHttpTracking.isEnabled, + ), + ], ), ), - - const SizedBox(height: 20), - - Wrap( - spacing: 10, - runSpacing: 10, - children: [ - ElevatedButton.icon( - onPressed: _setUserProperty, - icon: const Icon(Icons.person), - label: const Text('Set User Property'), - ), - ElevatedButton.icon( - onPressed: _simulateError, - icon: const Icon(Icons.error_outline), - label: const Text('Simulate Error'), - ), - ElevatedButton.icon( - onPressed: _navigateToSecondPage, - icon: const Icon(Icons.navigate_next), - label: const Text('Navigate'), - ), - ElevatedButton.icon( - onPressed: _navigateToViewTrackingExample, - icon: const Icon(Icons.visibility), - label: const Text('View Tracking'), - ), - ElevatedButton.icon( - onPressed: _navigateToHttpTrackingExample, - icon: const Icon(Icons.http), - label: const Text('HTTP Tracking'), - ), - ], - ), - ], - ), - ), - floatingActionButton: FloatingActionButton( - onPressed: _incrementCounter, - tooltip: 'Increment', - child: const Icon(Icons.add), - ), - ); - } - - Widget _buildStatusRow(String service, bool isEnabled) { - return Padding( - padding: const EdgeInsets.symmetric(vertical: 4), - child: Row( - children: [ - Icon(isEnabled ? Icons.check_circle : Icons.cancel, color: isEnabled ? Colors.green : Colors.red, size: 20), - const SizedBox(width: 8), - Text('$service: ${isEnabled ? "Enabled" : "Disabled"}'), + ), + + const SizedBox(height: 20), + + Wrap( + spacing: 10, + runSpacing: 10, + children: [ + ElevatedButton.icon( + onPressed: _setUserProperty, + icon: const Icon(Icons.person), + label: const Text('Set User Property'), + ), + ElevatedButton.icon( + onPressed: _simulateError, + icon: const Icon(Icons.error_outline), + label: const Text('Simulate Error'), + ), + ElevatedButton.icon( + onPressed: _navigateToSecondPage, + icon: const Icon(Icons.navigate_next), + label: const Text('Navigate'), + ), + ElevatedButton.icon( + onPressed: _navigateToViewTrackingExample, + icon: const Icon(Icons.visibility), + label: const Text('View Tracking'), + ), + ElevatedButton.icon( + onPressed: _navigateToHttpTrackingExample, + icon: const Icon(Icons.http), + label: const Text('HTTP Tracking'), + ), + ], + ), ], ), - ); - } + ), + floatingActionButton: FloatingActionButton( + onPressed: _incrementCounter, + tooltip: 'Increment', + child: const Icon(Icons.add), + ), + ); + + Widget _buildStatusRow(final String service, final bool isEnabled) => Padding( + padding: const EdgeInsets.symmetric(vertical: 4), + child: Row( + children: [ + Icon( + isEnabled ? Icons.check_circle : Icons.cancel, + color: isEnabled ? Colors.green : Colors.red, + size: 20, + ), + const SizedBox(width: 8), + Text('$service: ${isEnabled ? "Enabled" : "Disabled"}'), + ], + ), + ); } class SecondPage extends StatefulWidget { @@ -301,25 +352,32 @@ class _SecondPageState extends State { @override void initState() { super.initState(); - EngineAnalytics.setPage('SecondPage', 'HomePage'); + unawaited(EngineAnalytics.setPage('SecondPage', 'HomePage')); } @override - Widget build(BuildContext context) { - return Scaffold( - appBar: AppBar(title: const Text('Second Page'), backgroundColor: Theme.of(context).colorScheme.inversePrimary), - body: const Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Icon(Icons.star, size: 100, color: Colors.amber), - SizedBox(height: 20), - Text('Second Page', style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold)), - SizedBox(height: 20), - Text('This navigation was tracked!', style: TextStyle(fontSize: 16)), - ], - ), + Widget build(final BuildContext context) => Scaffold( + appBar: AppBar( + title: const Text('Second Page'), + backgroundColor: Theme.of(context).colorScheme.inversePrimary, + ), + body: const Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon(Icons.star, size: 100, color: Colors.amber), + SizedBox(height: 20), + Text( + 'Second Page', + style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold), + ), + SizedBox(height: 20), + Text( + 'This navigation was tracked!', + style: TextStyle(fontSize: 16), + ), + ], ), - ); - } + ), + ); } diff --git a/example/lib/view_tracking_example.dart b/example/lib/view_tracking_example.dart index 1531704..01b4b35 100644 --- a/example/lib/view_tracking_example.dart +++ b/example/lib/view_tracking_example.dart @@ -1,26 +1,26 @@ +// ignore_for_file: use_build_context_synchronously + +import 'dart:async'; + import 'package:engine_tracking/engine_tracking.dart'; import 'package:flutter/material.dart'; -/// Exemplo de uso do sistema de tracking de views do Engine Tracking class ViewTrackingExample extends StatelessWidget { const ViewTrackingExample({super.key}); @override - Widget build(BuildContext context) { - return MaterialApp( - title: 'View Tracking Example', - theme: ThemeData(primarySwatch: Colors.blue), - home: MainMenuPage(), - routes: { - '/profile': (context) => const ProfilePage(), - '/settings': (context) => SettingsPage(), - '/shopping': (context) => const ShoppingCartPage(), - }, - ); - } + Widget build(final BuildContext context) => MaterialApp( + title: 'View Tracking Example', + theme: ThemeData(primarySwatch: Colors.blue), + home: MainMenuPage(), + routes: { + '/profile': (final context) => const ProfilePage(), + '/settings': (final context) => SettingsPage(), + '/shopping': (final context) => const ShoppingCartPage(), + }, + ); } -/// Exemplo usando classe base EngineStatelessWidget class MainMenuPage extends EngineStatelessWidget { MainMenuPage({super.key}); @@ -28,94 +28,114 @@ class MainMenuPage extends EngineStatelessWidget { String get screenName => 'main_menu'; @override - Map? get screenParameters => {'app_version': '1.0.0', 'source': 'app_launch'}; + Map? get screenParameters => { + 'app_version': '1.0.0', + 'source': 'app_launch', + }; @override - Widget buildWithTracking(BuildContext context) { - return Scaffold( - appBar: AppBar(title: const Text('Menu Principal')), - body: Padding( - padding: const EdgeInsets.all(16.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - const Text( - 'Exemplo de Tracking de Views', - style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold), - textAlign: TextAlign.center, - ), - const SizedBox(height: 32), - ElevatedButton( - onPressed: () { - logUserAction( - 'navigate_to_profile', - parameters: {'navigation_method': 'button_tap', 'source_screen': screenName}, - ); - Navigator.pushNamed(context, '/profile'); - }, - child: const Text('Perfil do UsuĆ”rio'), - ), - const SizedBox(height: 16), - ElevatedButton( - onPressed: () { - logUserAction( - 'navigate_to_settings', - parameters: {'navigation_method': 'button_tap', 'source_screen': screenName}, - ); - Navigator.pushNamed(context, '/settings'); - }, - child: const Text('ConfiguraƧƵes'), - ), - const SizedBox(height: 16), - ElevatedButton( - onPressed: () { - logUserAction( - 'navigate_to_shopping', - parameters: {'navigation_method': 'button_tap', 'source_screen': screenName}, + Widget buildWithTracking(final BuildContext context) => Scaffold( + appBar: AppBar(title: const Text('Menu Principal')), + body: Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + const Text( + 'Exemplo de Tracking de Views', + style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold), + textAlign: TextAlign.center, + ), + const SizedBox(height: 32), + ElevatedButton( + onPressed: () async { + await logUserAction( + 'navigate_to_profile', + parameters: { + 'navigation_method': 'button_tap', + 'source_screen': screenName, + }, + ); + unawaited(Navigator.pushNamed(context, '/profile')); + }, + child: const Text('Perfil do UsuĆ”rio'), + ), + const SizedBox(height: 16), + ElevatedButton( + onPressed: () async { + await logUserAction( + 'navigate_to_settings', + parameters: { + 'navigation_method': 'button_tap', + 'source_screen': screenName, + }, + ); + unawaited(Navigator.pushNamed(context, '/settings')); + }, + child: const Text('ConfiguraƧƵes'), + ), + const SizedBox(height: 16), + ElevatedButton( + onPressed: () async { + await logUserAction( + 'navigate_to_shopping', + parameters: { + 'navigation_method': 'button_tap', + 'source_screen': screenName, + }, + ); + unawaited(Navigator.pushNamed(context, '/shopping')); + }, + child: const Text('Carrinho de Compras'), + ), + const SizedBox(height: 32), + ElevatedButton( + style: ElevatedButton.styleFrom(backgroundColor: Colors.orange), + onPressed: () async { + await logCustomEvent( + 'feature_demo', + parameters: { + 'feature_type': 'premium_feature', + 'user_type': 'free', + }, + ); + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('Evento customizado registrado!'), + ), + ); + }, + child: const Text('Demonstrar Evento Customizado'), + ), + const SizedBox(height: 16), + ElevatedButton( + style: ElevatedButton.styleFrom(backgroundColor: Colors.red), + onPressed: () async { + try { + throw Exception('Erro demonstrativo'); + } catch (e, stackTrace) { + await logScreenError( + 'Erro simulado para demonstração', + exception: e, + stackTrace: stackTrace, + additionalData: { + 'error_type': 'demonstration', + 'user_triggered': true, + }, ); - Navigator.pushNamed(context, '/shopping'); - }, - child: const Text('Carrinho de Compras'), - ), - const SizedBox(height: 32), - ElevatedButton( - style: ElevatedButton.styleFrom(backgroundColor: Colors.orange), - onPressed: () { - logCustomEvent('feature_demo', parameters: {'feature_type': 'premium_feature', 'user_type': 'free'}); - ScaffoldMessenger.of( - context, - ).showSnackBar(const SnackBar(content: Text('Evento customizado registrado!'))); - }, - child: const Text('Demonstrar Evento Customizado'), - ), - const SizedBox(height: 16), - ElevatedButton( - style: ElevatedButton.styleFrom(backgroundColor: Colors.red), - onPressed: () { - try { - throw Exception('Erro demonstrativo'); - } catch (e, stackTrace) { - logScreenError( - 'Erro simulado para demonstração', - exception: e, - stackTrace: stackTrace, - additionalData: {'error_type': 'demonstration', 'user_triggered': true}, - ); - } - ScaffoldMessenger.of( - context, - ).showSnackBar(const SnackBar(content: Text('Erro registrado no sistema!'))); - }, - child: const Text('Simular Erro'), - ), - ], - ), + } + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('Erro registrado no sistema!')), + ); + }, + child: const Text('Simular Erro'), + ), + ], ), - ); - } + ), + ); } -/// Exemplo usando classe base EngineStatefulWidget class ProfilePage extends EngineStatefulWidget { const ProfilePage({super.key}); @@ -145,87 +165,104 @@ class _ProfilePageState extends EngineStatefulWidgetState { } @override - Widget buildWithTracking(BuildContext context) { - return Scaffold( - appBar: AppBar(title: const Text('Perfil do UsuĆ”rio')), - body: Padding( - padding: const EdgeInsets.all(16.0), - child: Column( - children: [ - TextField( - decoration: const InputDecoration(labelText: 'Nome', border: OutlineInputBorder()), - onChanged: (value) { - setState(() { - _name = value; - }); - - logStateChange( - 'name_field_changed', - additionalData: {'character_count': value.length, 'has_content': value.isNotEmpty}, - ); - }, - onSubmitted: (value) { - logUserAction('field_completed', parameters: {'field': 'name', 'character_count': value.length}); - }, - ), - const SizedBox(height: 16), - TextField( - decoration: const InputDecoration(labelText: 'Email', border: OutlineInputBorder()), - onChanged: (value) { - setState(() { - _email = value; - }); - - logStateChange( - 'email_field_changed', - additionalData: {'character_count': value.length, 'is_valid_format': value.contains('@')}, - ); - }, - ), - const SizedBox(height: 16), - SwitchListTile( - title: const Text('Receber NotificaƧƵes'), - value: _notificationsEnabled, - onChanged: (value) { - setState(() { - _notificationsEnabled = value; - }); - - logUserAction( - 'notification_preference_changed', - parameters: {'enabled': value, 'source': 'profile_settings'}, - ); - }, + Widget buildWithTracking(final BuildContext context) => Scaffold( + appBar: AppBar(title: const Text('Perfil do UsuĆ”rio')), + body: Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + children: [ + TextField( + decoration: const InputDecoration( + labelText: 'Nome', + border: OutlineInputBorder(), ), - const SizedBox(height: 32), - ElevatedButton( - onPressed: (_name.isNotEmpty && _email.isNotEmpty) - ? () { - logUserAction( - 'profile_save_attempted', - parameters: { - 'name_length': _name.length, - 'email_length': _email.length, - 'notifications_enabled': _notificationsEnabled, - 'completion_percentage': _getProfileCompletion(), - }, - ); - - ScaffoldMessenger.of( - context, - ).showSnackBar(const SnackBar(content: Text('Perfil salvo com sucesso!'))); - } - : null, - child: const Text('Salvar Perfil'), + onChanged: (final value) async { + setState(() { + _name = value; + }); + + await logStateChange( + 'name_field_changed', + additionalData: { + 'character_count': value.length, + 'has_content': value.isNotEmpty, + }, + ); + }, + onSubmitted: (final value) async { + await logUserAction( + 'field_completed', + parameters: { + 'field': 'name', + 'character_count': value.length, + }, + ); + }, + ), + const SizedBox(height: 16), + TextField( + decoration: const InputDecoration( + labelText: 'Email', + border: OutlineInputBorder(), ), - ], - ), + onChanged: (final value) async { + setState(() { + _email = value; + }); + + await logStateChange( + 'email_field_changed', + additionalData: { + 'character_count': value.length, + 'is_valid_format': value.contains('@'), + }, + ); + }, + ), + const SizedBox(height: 16), + SwitchListTile( + title: const Text('Receber NotificaƧƵes'), + value: _notificationsEnabled, + onChanged: (final value) async { + setState(() { + _notificationsEnabled = value; + }); + + await logUserAction( + 'notification_preference_changed', + parameters: {'enabled': value, 'source': 'profile_settings'}, + ); + }, + ), + const SizedBox(height: 32), + ElevatedButton( + onPressed: (_name.isNotEmpty && _email.isNotEmpty) + ? () async { + await logUserAction( + 'profile_save_attempted', + parameters: { + 'name_length': _name.length, + 'email_length': _email.length, + 'notifications_enabled': _notificationsEnabled, + 'completion_percentage': _getProfileCompletion(), + }, + ); + + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('Perfil salvo com sucesso!'), + ), + ); + } + : null, + child: const Text('Salvar Perfil'), + ), + ], ), - ); - } + ), + ); } -/// Exemplo usando classe base para StatelessWidget class SettingsPage extends EngineStatelessWidget { SettingsPage({super.key}); @@ -233,58 +270,73 @@ class SettingsPage extends EngineStatelessWidget { String get screenName => 'app_settings'; @override - Widget buildWithTracking(BuildContext context) { - return Scaffold( - appBar: AppBar(title: const Text('ConfiguraƧƵes')), - body: ListView( - children: [ - ListTile( - leading: const Icon(Icons.notifications), - title: const Text('NotificaƧƵes'), - subtitle: const Text('Gerenciar preferĆŖncias de notificação'), - onTap: () { - logUserAction('settings_item_tapped', parameters: {'setting_type': 'notifications', 'item_position': 0}); - }, - ), - ListTile( - leading: const Icon(Icons.privacy_tip), - title: const Text('Privacidade'), - subtitle: const Text('ConfiguraƧƵes de privacidade e dados'), - onTap: () { - logUserAction('settings_item_tapped', parameters: {'setting_type': 'privacy', 'item_position': 1}); - }, - ), - ListTile( - leading: const Icon(Icons.security), - title: const Text('SeguranƧa'), - subtitle: const Text('ConfiguraƧƵes de seguranƧa da conta'), - onTap: () { - logUserAction('settings_item_tapped', parameters: {'setting_type': 'security', 'item_position': 2}); - }, - ), - ListTile( - leading: const Icon(Icons.help), - title: const Text('Ajuda'), - subtitle: const Text('Central de ajuda e suporte'), - onTap: () { - logUserAction('settings_item_tapped', parameters: {'setting_type': 'help', 'item_position': 3}); - }, - ), - const Divider(), - ListTile( - leading: const Icon(Icons.info), - title: const Text('Sobre o App'), - onTap: () { - logCustomEvent('about_app_accessed', parameters: {'source_screen': screenName}); - }, - ), - ], - ), - ); - } + Widget buildWithTracking(final BuildContext context) => Scaffold( + appBar: AppBar(title: const Text('ConfiguraƧƵes')), + body: ListView( + children: [ + ListTile( + leading: const Icon(Icons.notifications), + title: const Text('NotificaƧƵes'), + subtitle: const Text('Gerenciar preferĆŖncias de notificação'), + onTap: () async { + await logUserAction( + 'settings_item_tapped', + parameters: { + 'setting_type': 'notifications', + 'item_position': 0, + }, + ); + }, + ), + ListTile( + leading: const Icon(Icons.privacy_tip), + title: const Text('Privacidade'), + subtitle: const Text('ConfiguraƧƵes de privacidade e dados'), + onTap: () async { + await logUserAction( + 'settings_item_tapped', + parameters: {'setting_type': 'privacy', 'item_position': 1}, + ); + }, + ), + ListTile( + leading: const Icon(Icons.security), + title: const Text('SeguranƧa'), + subtitle: const Text('ConfiguraƧƵes de seguranƧa da conta'), + onTap: () async { + await logUserAction( + 'settings_item_tapped', + parameters: {'setting_type': 'security', 'item_position': 2}, + ); + }, + ), + ListTile( + leading: const Icon(Icons.help), + title: const Text('Ajuda'), + subtitle: const Text('Central de ajuda e suporte'), + onTap: () async { + await logUserAction( + 'settings_item_tapped', + parameters: {'setting_type': 'help', 'item_position': 3}, + ); + }, + ), + const Divider(), + ListTile( + leading: const Icon(Icons.info), + title: const Text('Sobre o App'), + onTap: () async { + await logCustomEvent( + 'about_app_accessed', + parameters: {'source_screen': screenName}, + ); + }, + ), + ], + ), + ); } -/// Exemplo usando classe base para StatefulWidget class ShoppingCartPage extends EngineStatefulWidget { const ShoppingCartPage({super.key}); @@ -309,183 +361,220 @@ class _ShoppingCartPageState extends EngineStatefulWidgetState 'has_items': _products.isNotEmpty, }; - double _calculateTotal() { - return _products.fold(0.0, (sum, product) => sum + (product['price'] as double) * (product['quantity'] as int)); - } + double _calculateTotal() => _products.fold( + 0.0, + (final sum, final product) => sum + (product['price'] as double) * (product['quantity'] as int), + ); @override - Widget buildWithTracking(BuildContext context) { - return Scaffold( - appBar: AppBar( - title: const Text('Carrinho'), - actions: [ - IconButton( - icon: const Icon(Icons.delete_sweep), - onPressed: _products.isNotEmpty - ? () { - logUserAction( - 'clear_cart_pressed', - parameters: {'items_to_remove': _products.length, 'total_value_lost': _calculateTotal()}, - ); + Widget buildWithTracking(final BuildContext context) => Scaffold( + appBar: AppBar( + title: const Text('Carrinho'), + actions: [ + IconButton( + icon: const Icon(Icons.delete_sweep), + onPressed: _products.isNotEmpty + ? () async { + await logUserAction( + 'clear_cart_pressed', + parameters: { + 'items_to_remove': _products.length, + 'total_value_lost': _calculateTotal(), + }, + ); - setState(() { - _products.clear(); - }); + setState(_products.clear); - logStateChange('cart_cleared', additionalData: {'action': 'clear_all', 'remaining_items': 0}); - } - : null, - ), - ], - ), - body: _products.isEmpty - ? const Center( - child: Text('Carrinho vazio', style: TextStyle(fontSize: 18, color: Colors.grey)), - ) - : Column( - children: [ - Expanded( - child: ListView.builder( - itemCount: _products.length, - itemBuilder: (context, index) { - final product = _products[index]; - return Card( - margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), - child: ListTile( - title: Text(product['name']), - subtitle: Text('R\$ ${product['price'].toStringAsFixed(2)} x ${product['quantity']}'), - trailing: Row( - mainAxisSize: MainAxisSize.min, - children: [ - IconButton( - icon: const Icon(Icons.remove), - onPressed: () { - setState(() { - if (product['quantity'] > 1) { - product['quantity']--; - } - }); - - logUserAction( - 'quantity_decreased', - parameters: { - 'product_id': product['id'], - 'product_name': product['name'], - 'new_quantity': product['quantity'], - }, - ); - }, - ), - Text('${product['quantity']}'), - IconButton( - icon: const Icon(Icons.add), - onPressed: () { - setState(() { - product['quantity']++; - }); - - logUserAction( - 'quantity_increased', - parameters: { - 'product_id': product['id'], - 'product_name': product['name'], - 'new_quantity': product['quantity'], - }, - ); - }, - ), - IconButton( - icon: const Icon(Icons.delete, color: Colors.red), - onPressed: () { - final removedProduct = _products[index]; - setState(() { - _products.removeAt(index); - }); - - logUserAction( - 'product_removed', - parameters: { - 'product_id': removedProduct['id'], - 'product_name': removedProduct['name'], - 'quantity_removed': removedProduct['quantity'], - 'value_lost': removedProduct['price'] * removedProduct['quantity'], - }, - ); - - logStateChange( - 'cart_updated', - additionalData: { - 'action': 'item_removal', - 'remaining_items': _products.length, - 'new_total': _calculateTotal(), - }, - ); - }, + await logStateChange( + 'cart_cleared', + additionalData: { + 'action': 'clear_all', + 'remaining_items': 0, + }, + ); + } + : null, + ), + ], + ), + body: _products.isEmpty + ? const Center( + child: Text( + 'Carrinho vazio', + style: TextStyle(fontSize: 18, color: Colors.grey), + ), + ) + : Column( + children: [ + Expanded( + child: ListView.builder( + itemCount: _products.length, + itemBuilder: (final context, final index) { + final product = _products[index]; + return Card( + margin: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 8, + ), + child: ListTile( + title: Text(product['name'] as String), + subtitle: Text( + // ignore: avoid_dynamic_calls + 'R\$ ${product['price']?.toStringAsFixed(2)} x ${product['quantity']}', + ), + trailing: Row( + mainAxisSize: MainAxisSize.min, + children: [ + IconButton( + icon: const Icon(Icons.remove), + onPressed: () async { + setState(() { + if ((product['quantity'] as int) > 1) { + product['quantity'] = (product['quantity'] as int) - 1; + } + }); + + await logUserAction( + 'quantity_decreased', + parameters: { + 'product_id': product['id'], + 'product_name': product['name'], + 'new_quantity': product['quantity'], + }, + ); + }, + ), + Text('${product['quantity']}'), + IconButton( + icon: const Icon(Icons.add), + onPressed: () async { + setState(() { + product['quantity'] = (product['quantity'] as int) + 1; + }); + + await logUserAction( + 'quantity_increased', + parameters: { + 'product_id': product['id'], + 'product_name': product['name'], + 'new_quantity': product['quantity'], + }, + ); + }, + ), + IconButton( + icon: const Icon( + Icons.delete, + color: Colors.red, ), - ], - ), + onPressed: () async { + final removedProduct = _products[index]; + setState(() { + _products.removeAt(index); + }); + + await logUserAction( + 'product_removed', + parameters: { + 'product_id': removedProduct['id'], + 'product_name': removedProduct['name'], + 'quantity_removed': removedProduct['quantity'], + 'value_lost': + (removedProduct['price'] as double) * (removedProduct['quantity'] as int), + }, + ); + + await logStateChange( + 'cart_updated', + additionalData: { + 'action': 'item_removal', + 'remaining_items': _products.length, + 'new_total': _calculateTotal(), + }, + ); + }, + ), + ], ), - ); - }, - ), + ), + ); + }, ), - Container( - padding: const EdgeInsets.all(16), - decoration: BoxDecoration( - color: Colors.grey[100], - border: Border(top: BorderSide(color: Colors.grey[300]!)), - ), - child: Column( - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - const Text('Total:', style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)), - Text( - 'R\$ ${_calculateTotal().toStringAsFixed(2)}', - style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold), + ), + Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: Colors.grey[100], + border: Border(top: BorderSide(color: Colors.grey[300]!)), + ), + child: Column( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + const Text( + 'Total:', + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, ), - ], - ), - const SizedBox(height: 16), - SizedBox( - width: double.infinity, - child: ElevatedButton( - onPressed: _products.isNotEmpty - ? () { - logUserAction( - 'checkout_initiated', - parameters: { - 'product_count': _products.length, - 'total_value': _calculateTotal(), - 'items': _products - .map((p) => {'id': p['id'], 'quantity': p['quantity']}) - .toList(), - }, - ); - - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar(content: Text('Checkout iniciado!'), backgroundColor: Colors.green), - ); - } - : null, - child: const Text('Finalizar Compra'), ), + Text( + 'R\$ ${_calculateTotal().toStringAsFixed(2)}', + style: const TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + ), + ), + ], + ), + const SizedBox(height: 16), + SizedBox( + width: double.infinity, + child: ElevatedButton( + onPressed: _products.isNotEmpty + ? () async { + await logUserAction( + 'checkout_initiated', + parameters: { + 'product_count': _products.length, + 'total_value': _calculateTotal(), + 'items': _products + .map( + (final p) => { + 'id': p['id'], + 'quantity': p['quantity'], + }, + ) + .toList(), + }, + ); + + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('Checkout iniciado!'), + backgroundColor: Colors.green, + ), + ); + } + : null, + child: const Text('Finalizar Compra'), ), - ], - ), + ), + ], ), - ], - ), - ); - } + ), + ], + ), + ); } -/// Função para demonstrar a inicialização do sistema Future initializeTrackingExample() async { final analyticsModel = EngineAnalyticsModel( - firebaseAnalyticsConfig: const EngineFirebaseAnalyticsConfig(enabled: false), - faroConfig: const EngineFaroConfig( + firebaseAnalyticsConfig: EngineFirebaseAnalyticsConfig( + enabled: false, + ), + faroConfig: EngineFaroConfig( enabled: false, endpoint: '', appName: '', @@ -495,7 +584,7 @@ Future initializeTrackingExample() async { namespace: '', platform: '', ), - splunkConfig: const EngineSplunkConfig( + splunkConfig: EngineSplunkConfig( enabled: false, endpoint: '', token: '', @@ -503,11 +592,8 @@ Future initializeTrackingExample() async { sourcetype: '', index: '', ), - clarityConfig: const EngineClarityConfig( - enabled: false, - projectId: '', - ), - googleLoggingConfig: const EngineGoogleLoggingConfig( + clarityConfig: EngineClarityConfig(enabled: false, projectId: ''), + googleLoggingConfig: EngineGoogleLoggingConfig( enabled: false, projectId: '', logName: '', @@ -516,8 +602,8 @@ Future initializeTrackingExample() async { ); final bugTrackingModel = EngineBugTrackingModel( - crashlyticsConfig: const EngineCrashlyticsConfig(enabled: false), - faroConfig: const EngineFaroConfig( + crashlyticsConfig: EngineCrashlyticsConfig(enabled: false), + faroConfig: EngineFaroConfig( enabled: false, endpoint: '', appName: '', @@ -527,7 +613,7 @@ Future initializeTrackingExample() async { namespace: '', platform: '', ), - googleLoggingConfig: const EngineGoogleLoggingConfig( + googleLoggingConfig: EngineGoogleLoggingConfig( enabled: false, projectId: '', logName: '', @@ -538,5 +624,5 @@ Future initializeTrackingExample() async { await EngineAnalytics.initWithModel(analyticsModel); await EngineBugTracking.initWithModel(bugTrackingModel); - print('Sistema de tracking inicializado para exemplo!'); + debugPrint('Sistema de tracking inicializado para exemplo!'); } diff --git a/lib/src/analytics/adapters/adapters.dart b/lib/src/analytics/adapters/adapters.dart index c689df1..8b82d5b 100644 --- a/lib/src/analytics/adapters/adapters.dart +++ b/lib/src/analytics/adapters/adapters.dart @@ -1,3 +1,4 @@ +export 'engine_clarity_adapter.dart'; export 'engine_faro_analytics_adapter.dart'; export 'engine_firebase_analytics_adapter.dart'; export 'engine_google_logging_analytics_adapter.dart'; diff --git a/lib/src/analytics/adapters/engine_clarity_adapter.dart b/lib/src/analytics/adapters/engine_clarity_adapter.dart new file mode 100644 index 0000000..ad5c071 --- /dev/null +++ b/lib/src/analytics/adapters/engine_clarity_adapter.dart @@ -0,0 +1,82 @@ +import 'package:clarity_flutter/clarity_flutter.dart'; +import 'package:engine_tracking/engine_tracking.dart'; +import 'package:flutter/widgets.dart'; + +class EngineClarityAdapter implements IEngineAnalyticsAdapter { + EngineClarityAdapter(this.config); + + @override + final EngineClarityConfig config; + + @override + String get adapterName => 'Microsoft Clarity'; + + @override + bool get isEnabled => config.enabled; + + @override + bool get isInitialized => _isInitialized; + + bool get isFaroInitialized => isEnabled && _isInitialized; + + bool _isInitialized = false; + + @override + Future dispose() => Future.value(null); + + @override + Future initialize() async { + if (!isEnabled || _isInitialized) { + debugPrint('Clarity is not enabled or already initialized'); + return; + } + _isInitialized = true; + try { + Clarity.setCustomSessionId(EngineSession.instance.sessionId); + } catch (e) { + _isInitialized = false; + } + } + + @override + Future logAppOpen([final Map? parameters]) async { + Clarity.sendCustomEvent('open_app'); + await Future.value(null); + } + + @override + Future logEvent(final String name, [final Map? parameters]) async { + Clarity.sendCustomEvent(name); + await Future.value(null); + } + + @override + Future reset() async { + Clarity.setOnSessionStartedCallback((final sessionId) { + Clarity.setCustomSessionId(EngineSession.instance.sessionId); + }); + await Future.value(null); + } + + @override + Future setPage( + final String screenName, [ + final String? previousScreen, + final Map? parameters, + ]) async { + Clarity.setCurrentScreenName(screenName); + await Future.value(null); + } + + @override + Future setUserId(final String? userId, [final String? email, final String? name]) async { + Clarity.setCustomUserId(userId ?? email ?? name ?? ''); + await Future.value(null); + } + + @override + Future setUserProperty(final String name, final String? value) { + Clarity.setCustomTag(name, value ?? ''); + return Future.value(null); + } +} diff --git a/lib/src/analytics/adapters/engine_faro_analytics_adapter.dart b/lib/src/analytics/adapters/engine_faro_analytics_adapter.dart index e776f63..8943566 100644 --- a/lib/src/analytics/adapters/engine_faro_analytics_adapter.dart +++ b/lib/src/analytics/adapters/engine_faro_analytics_adapter.dart @@ -1,33 +1,28 @@ import 'package:engine_tracking/engine_tracking.dart'; -import 'package:faro/faro_sdk.dart'; +import 'package:faro/faro.dart'; import 'package:flutter/widgets.dart'; -class EngineFaroAnalyticsAdapter implements IEngineAnalyticsAdapter { - EngineFaroAnalyticsAdapter(this._config); +class EngineFaroAnalyticsAdapter implements IEngineAnalyticsAdapter { + EngineFaroAnalyticsAdapter(this.config); - final EngineFaroConfig _config; - bool _isInitialized = false; + @override + final EngineFaroConfig config; @override String get adapterName => 'Grafana Faro'; @override - bool get isEnabled => _config.enabled; + bool get isEnabled => config.enabled; @override bool get isInitialized => _isInitialized; + bool _isInitialized = false; + bool get isFaroInitialized => isEnabled && _isInitialized && _faro != null; Faro? _faro; - Map? _convertToStringMap(final Map? map) { - if (map == null) { - return null; - } - return map.map((final key, final value) => MapEntry(key, value.toString())); - } - @override Future initialize() async { if (!isEnabled || _isInitialized) { @@ -35,24 +30,24 @@ class EngineFaroAnalyticsAdapter implements IEngineAnalyticsAdapter { return; } + _isInitialized = true; + try { _faro = Faro(); if (!EngineBugTracking.isFaroInitialized) { await _faro?.init( optionsConfiguration: FaroConfig( - apiKey: _config.apiKey, - appName: _config.appName, - appVersion: _config.appVersion, - appEnv: _config.environment, - collectorUrl: _config.endpoint, + apiKey: config.apiKey, + appName: config.appName, + appVersion: config.appVersion, + appEnv: config.environment, + collectorUrl: config.endpoint, enableCrashReporting: true, anrTracking: true, - refreshRateVitals: true, - namespace: _config.namespace, + namespace: config.namespace, ), ); } - _isInitialized = true; } catch (e) { _isInitialized = false; } @@ -71,7 +66,7 @@ class EngineFaroAnalyticsAdapter implements IEngineAnalyticsAdapter { } try { - await _faro!.pushEvent(name, attributes: _convertToStringMap(parameters)); + _faro?.pushEvent(name, attributes: convertToStringMap(parameters)); } catch (e) { debugPrint('logEvent: Error logging event: $e'); } @@ -89,7 +84,7 @@ class EngineFaroAnalyticsAdapter implements IEngineAnalyticsAdapter { } try { - _faro!.setUserMeta(userId: userId, userEmail: email, userName: name); + _faro?.setUserMeta(userId: userId, userEmail: email, userName: name); } catch (e) { debugPrint('setUserId: Error setting user id: $e'); } @@ -115,7 +110,12 @@ class EngineFaroAnalyticsAdapter implements IEngineAnalyticsAdapter { } try { - _faro!.setViewMeta(name: screenName); + _faro?.setViewMeta(name: screenName); + _faro?.pushEvent( + 'navigation', + attributes: {...?parameters, 'screen': screenName, 'previousScreen': previousScreen ?? ''}, + trace: {'to_screen': screenName, 'previousScreen': previousScreen ?? ''}, + ); } catch (e) { debugPrint('setPage: Error setting page: $e'); } @@ -129,7 +129,7 @@ class EngineFaroAnalyticsAdapter implements IEngineAnalyticsAdapter { } try { - await _faro!.pushEvent('app_open', attributes: _convertToStringMap(parameters)); + _faro?.pushEvent('app_open', attributes: convertToStringMap(parameters)); } catch (e) { debugPrint('logAppOpen: Error logging app open: $e'); } @@ -137,13 +137,13 @@ class EngineFaroAnalyticsAdapter implements IEngineAnalyticsAdapter { @override Future reset() async { - if (!isFaroInitialized) { + if (isFaroInitialized) { debugPrint('reset: Faro is not initialized'); return; } try { - _faro!.setUserMeta(userId: null, userEmail: null, userName: null); + _faro?.setUserMeta(userId: null, userEmail: null, userName: null); } catch (e) { debugPrint('reset: Error resetting: $e'); } diff --git a/lib/src/analytics/adapters/engine_firebase_analytics_adapter.dart b/lib/src/analytics/adapters/engine_firebase_analytics_adapter.dart index c2be751..9286c78 100644 --- a/lib/src/analytics/adapters/engine_firebase_analytics_adapter.dart +++ b/lib/src/analytics/adapters/engine_firebase_analytics_adapter.dart @@ -1,26 +1,26 @@ -import 'package:engine_tracking/src/analytics/adapters/i_engine_analytics_adapter.dart'; -import 'package:engine_tracking/src/config/engine_firebase_analytics_config.dart'; -import 'package:engine_tracking/src/session/engine_session.dart'; +import 'package:engine_tracking/engine_tracking.dart'; import 'package:firebase_analytics/firebase_analytics.dart'; import 'package:flutter/material.dart'; -class EngineFirebaseAnalyticsAdapter implements IEngineAnalyticsAdapter { - EngineFirebaseAnalyticsAdapter(this._config); +class EngineFirebaseAnalyticsAdapter implements IEngineAnalyticsAdapter { + EngineFirebaseAnalyticsAdapter(this.config); - bool _isInitialized = false; + @override + final EngineFirebaseAnalyticsConfig config; @override String get adapterName => 'Firebase Analytics'; @override - bool get isEnabled => _config.enabled; + bool get isEnabled => config.enabled; @override bool get isInitialized => _isInitialized; bool get isFirebaseAnalyticsInitialized => isEnabled && _isInitialized && _firebaseAnalytics != null; - final EngineFirebaseAnalyticsConfig _config; + bool _isInitialized = false; + FirebaseAnalytics? _firebaseAnalytics; @override @@ -29,9 +29,10 @@ class EngineFirebaseAnalyticsAdapter implements IEngineAnalyticsAdapter { return; } + _isInitialized = true; + try { _firebaseAnalytics = FirebaseAnalytics.instance; - _isInitialized = true; } catch (e) { _isInitialized = false; } diff --git a/lib/src/analytics/adapters/engine_google_logging_analytics_adapter.dart b/lib/src/analytics/adapters/engine_google_logging_analytics_adapter.dart index 3de7747..30e523b 100644 --- a/lib/src/analytics/adapters/engine_google_logging_analytics_adapter.dart +++ b/lib/src/analytics/adapters/engine_google_logging_analytics_adapter.dart @@ -3,26 +3,29 @@ import 'package:flutter/material.dart'; import 'package:googleapis/logging/v2.dart' as logging; import 'package:googleapis_auth/auth_io.dart' as auth; -class EngineGoogleLoggingAnalyticsAdapter implements IEngineAnalyticsAdapter { - EngineGoogleLoggingAnalyticsAdapter(this._config); +class EngineGoogleLoggingAnalyticsAdapter implements IEngineAnalyticsAdapter { + EngineGoogleLoggingAnalyticsAdapter(this.config); - bool _isInitialized = false; + @override + final EngineGoogleLoggingConfig config; @override String get adapterName => 'Google Cloud Logging Analytics'; @override - bool get isEnabled => _config.enabled; + bool get isEnabled => config.enabled; @override bool get isInitialized => _isInitialized; bool get isGoogleLoggingAnalyticsInitialized => isEnabled && _isInitialized; - final EngineGoogleLoggingConfig _config; + bool _isInitialized = false; + + String? _userId; + late final logging.LoggingApi _loggingApi; late final auth.AuthClient _authClient; - String? _userId; @override Future initialize() async { @@ -30,14 +33,15 @@ class EngineGoogleLoggingAnalyticsAdapter implements IEngineAnalyticsAdapter { return; } + _isInitialized = true; + try { _authClient = await auth.clientViaServiceAccount( - auth.ServiceAccountCredentials.fromJson(_config.credentials), + auth.ServiceAccountCredentials.fromJson(config.credentials), [logging.LoggingApi.loggingWriteScope], ); _loggingApi = logging.LoggingApi(_authClient); - _isInitialized = true; } catch (e) { debugPrint('initialize: Error initializing Google Cloud Logging: $e'); _isInitialized = false; @@ -69,9 +73,9 @@ class EngineGoogleLoggingAnalyticsAdapter implements IEngineAnalyticsAdapter { 'timestamp': DateTime.now().toIso8601String(), } ..severity = 'INFO' - ..logName = 'projects/${_config.projectId}/logs/${_config.logName}-analytics' + ..logName = 'projects/${config.projectId}/logs/${config.logName}-analytics' ..resource = logging.MonitoredResource.fromJson( - _config.resource ?? + config.resource ?? { 'type': 'global', }, @@ -79,7 +83,7 @@ class EngineGoogleLoggingAnalyticsAdapter implements IEngineAnalyticsAdapter { final request = logging.WriteLogEntriesRequest() ..entries = [logEntry] - ..logName = 'projects/${_config.projectId}/logs/${_config.logName}-analytics'; + ..logName = 'projects/${config.projectId}/logs/${config.logName}-analytics'; await _loggingApi.entries.write(request); } catch (e) { diff --git a/lib/src/analytics/adapters/engine_splunk_analytics_adapter.dart b/lib/src/analytics/adapters/engine_splunk_analytics_adapter.dart index 4786f14..c792f4d 100644 --- a/lib/src/analytics/adapters/engine_splunk_analytics_adapter.dart +++ b/lib/src/analytics/adapters/engine_splunk_analytics_adapter.dart @@ -5,14 +5,17 @@ import 'package:engine_tracking/src/analytics/adapters/i_engine_analytics_adapte import 'package:engine_tracking/src/config/engine_splunk_config.dart'; import 'package:flutter/material.dart'; -class EngineSplunkAnalyticsAdapter implements IEngineAnalyticsAdapter { - EngineSplunkAnalyticsAdapter(this._config); +class EngineSplunkAnalyticsAdapter implements IEngineAnalyticsAdapter { + EngineSplunkAnalyticsAdapter(this.config); + + @override + final EngineSplunkConfig config; @override String get adapterName => 'Splunk'; @override - bool get isEnabled => _config.enabled; + bool get isEnabled => config.enabled; @override bool get isInitialized => _isInitialized; @@ -20,7 +23,7 @@ class EngineSplunkAnalyticsAdapter implements IEngineAnalyticsAdapter { bool get isSplunkInitialized => isEnabled && _isInitialized; bool _isInitialized = false; - final EngineSplunkConfig _config; + late final HttpClient _httpClient; @override @@ -29,9 +32,10 @@ class EngineSplunkAnalyticsAdapter implements IEngineAnalyticsAdapter { return; } + _isInitialized = true; + try { _httpClient = HttpClient(); - _isInitialized = true; } catch (e) { _isInitialized = false; debugPrint('failed to initialize Splunk $e'); @@ -95,11 +99,8 @@ class EngineSplunkAnalyticsAdapter implements IEngineAnalyticsAdapter { try { await _sendToSplunk({ - 'event': 'user_property_set', - 'data': { - 'property_name': name, - 'property_value': value, - }, + 'event': name, + 'data': value, 'timestamp': DateTime.now().millisecondsSinceEpoch / 1000, }); } catch (e) { @@ -169,16 +170,16 @@ class EngineSplunkAnalyticsAdapter implements IEngineAnalyticsAdapter { } Future _sendToSplunk(final Map data) async { - final request = await _httpClient.postUrl(Uri.parse(_config.endpoint)); + final request = await _httpClient.postUrl(Uri.parse(config.endpoint)); - request.headers.set('Authorization', 'Splunk ${_config.token}'); + request.headers.set('Authorization', 'Splunk ${config.token}'); request.headers.set('Content-Type', 'application/json'); final body = jsonEncode({ 'time': data['timestamp'], - 'source': _config.source, - 'sourcetype': _config.sourcetype, - 'index': _config.index, + 'source': config.source, + 'sourcetype': config.sourcetype, + 'index': config.index, 'event': data, }); diff --git a/lib/src/analytics/adapters/i_engine_analytics_adapter.dart b/lib/src/analytics/adapters/i_engine_analytics_adapter.dart index 39dab7d..67804f4 100644 --- a/lib/src/analytics/adapters/i_engine_analytics_adapter.dart +++ b/lib/src/analytics/adapters/i_engine_analytics_adapter.dart @@ -1,7 +1,10 @@ -abstract class IEngineAnalyticsAdapter { +import 'package:engine_tracking/src/config/config.dart'; + +abstract class IEngineAnalyticsAdapter { String get adapterName; bool get isEnabled; bool get isInitialized; + TConfig get config; Future initialize(); Future dispose(); diff --git a/lib/src/analytics/engine_analytics.dart b/lib/src/analytics/engine_analytics.dart index e2212a3..ef515db 100644 --- a/lib/src/analytics/engine_analytics.dart +++ b/lib/src/analytics/engine_analytics.dart @@ -9,19 +9,39 @@ class EngineAnalytics { static bool get isEnabled => _adapters.isNotEmpty; static bool get isInitialized => _isInitialized; - static bool get isFaroInitialized => - isAdapterInitialized((final adapter) => adapter is EngineFaroAnalyticsAdapter && adapter.isInitialized); - static bool get isFirebaseInitialized => - isAdapterInitialized((final adapter) => adapter is EngineFirebaseAnalyticsAdapter && adapter.isInitialized); - static bool get isGoogleLoggingInitialized => - isAdapterInitialized((final adapter) => adapter is EngineGoogleLoggingAnalyticsAdapter && adapter.isInitialized); - static bool get isSplunkInitialized => - isAdapterInitialized((final adapter) => adapter is EngineSplunkAnalyticsAdapter && adapter.isInitialized); + + static bool get isClarityInitialized => _isAdapterTypeInitialized(); + static bool get isFaroInitialized => _isAdapterTypeInitialized(); + static bool get isFirebaseInitialized => _isAdapterTypeInitialized(); + static bool get isGoogleLoggingInitialized => _isAdapterTypeInitialized(); + static bool get isSplunkInitialized => _isAdapterTypeInitialized(); static final _adapters = []; static bool isAdapterInitialized(final PredicateAnalytics predicate) => _adapters.any(predicate); + static bool _isAdapterTypeInitialized() => + _adapters.whereType().any((final adapter) => adapter.isInitialized); + + /// Retrieves the configuration for a specific adapter type. + /// + /// Returns the configuration if an enabled adapter of type [TAdapter] is found, + /// otherwise returns null. + /// + /// Example: + /// ```dart + /// final config = EngineAnalytics.getConfig(); + /// ``` + static TConfig? getConfig() { + final enabledAdapter = _adapters.whereType().where((final adapter) => adapter.isEnabled).firstOrNull; + + if (enabledAdapter?.config is TConfig) { + return enabledAdapter!.config as TConfig; + } + + return null; + } + static Future init(final List adapters) async { if (_isInitialized) { return; @@ -31,19 +51,19 @@ class EngineAnalytics { ..clear() ..addAll(adapters.where((final adapter) => adapter.isEnabled)); - final futures = _adapters.map((final adapter) => adapter.initialize()).toList(); - - await Future.wait(futures); + final initializes = _adapters.map((final adapter) => adapter.initialize()); + await Future.wait(initializes); _isInitialized = true; } static Future initWithModel(final EngineAnalyticsModel model) async { final adapters = [ - EngineFirebaseAnalyticsAdapter(model.firebaseAnalyticsConfig), - EngineFaroAnalyticsAdapter(model.faroConfig), - EngineGoogleLoggingAnalyticsAdapter(model.googleLoggingConfig), - EngineSplunkAnalyticsAdapter(model.splunkConfig), + if (model.clarityConfig != null) EngineClarityAdapter(model.clarityConfig!), + if (model.firebaseAnalyticsConfig != null) EngineFirebaseAnalyticsAdapter(model.firebaseAnalyticsConfig!), + if (model.faroConfig != null) EngineFaroAnalyticsAdapter(model.faroConfig!), + if (model.googleLoggingConfig != null) EngineGoogleLoggingAnalyticsAdapter(model.googleLoggingConfig!), + if (model.splunkConfig != null) EngineSplunkAnalyticsAdapter(model.splunkConfig!), ]; await init(adapters); @@ -54,20 +74,16 @@ class EngineAnalytics { return; } - final futures = _adapters - .map( - (final adapter) => adapter.dispose(), - ) - .toList(); - await Future.wait(futures); + final disposes = _adapters.map((final adapter) => adapter.dispose()); + await Future.wait(disposes); _adapters.clear(); _isInitialized = false; } static Future reset() async { - final futures = _adapters.map((final adapter) => adapter.reset()).toList(); - await Future.wait(futures); + final resets = _adapters.map((final adapter) => adapter.reset()); + await Future.wait(resets); } static Future logEvent(final String name, [final Map? parameters]) async { @@ -75,12 +91,8 @@ class EngineAnalytics { return; } - final futures = _adapters - .map( - (final adapter) => adapter.logEvent(name, parameters), - ) - .toList(); - await Future.wait(futures); + final logEvents = _adapters.map((final adapter) => adapter.logEvent(name, parameters)); + await Future.wait(logEvents); } static Future setUserId(final String? userId, [final String? email, final String? name]) async { @@ -88,12 +100,8 @@ class EngineAnalytics { return; } - final futures = _adapters - .map( - (final adapter) => adapter.setUserId(userId, email, name), - ) - .toList(); - await Future.wait(futures); + final setUserIds = _adapters.map((final adapter) => adapter.setUserId(userId, email, name)); + await Future.wait(setUserIds); } static Future setUserProperty(final String name, final String? value) async { @@ -101,8 +109,8 @@ class EngineAnalytics { return; } - final futures = _adapters.map((final adapter) => adapter.setUserProperty(name, value)); - await Future.wait(futures); + final setUserProperties = _adapters.map((final adapter) => adapter.setUserProperty(name, value)); + await Future.wait(setUserProperties); } static Future setPage( @@ -114,16 +122,8 @@ class EngineAnalytics { return; } - final futures = _adapters - .map( - (final adapter) => adapter.setPage( - screenName, - previousScreen, - parameters, - ), - ) - .toList(); - await Future.wait(futures); + final setPages = _adapters.map((final adapter) => adapter.setPage(screenName, previousScreen, parameters)); + await Future.wait(setPages); } static Future logAppOpen([final Map? parameters]) async { @@ -131,9 +131,7 @@ class EngineAnalytics { return; } - final futures = _adapters.map( - (final adapter) => adapter.logAppOpen(parameters), - ); - await Future.wait(futures); + final logAppOpens = _adapters.map((final adapter) => adapter.logAppOpen(parameters)); + await Future.wait(logAppOpens); } } diff --git a/lib/src/bug_tracking/adapters/engine_crashlytics_adapter.dart b/lib/src/bug_tracking/adapters/engine_crashlytics_adapter.dart index 639b95b..4ca1be9 100644 --- a/lib/src/bug_tracking/adapters/engine_crashlytics_adapter.dart +++ b/lib/src/bug_tracking/adapters/engine_crashlytics_adapter.dart @@ -3,22 +3,25 @@ import 'package:engine_tracking/src/config/engine_crashlytics_config.dart'; import 'package:firebase_crashlytics/firebase_crashlytics.dart'; import 'package:flutter/foundation.dart'; -class EngineCrashlyticsAdapter implements IEngineBugTrackingAdapter { - EngineCrashlyticsAdapter(this._config); +class EngineCrashlyticsAdapter implements IEngineBugTrackingAdapter { + EngineCrashlyticsAdapter(this.config); @override String get adapterName => 'Firebase Crashlytics'; @override - bool get isEnabled => _config.enabled; + bool get isEnabled => config.enabled; @override bool get isInitialized => _isInitialized; + @override + final EngineCrashlyticsConfig config; + bool get isCrashlyticsInitialized => isEnabled && _isInitialized && _crashlytics != null; bool _isInitialized = false; - final EngineCrashlyticsConfig _config; + FirebaseCrashlytics? _crashlytics; @override @@ -27,10 +30,11 @@ class EngineCrashlyticsAdapter implements IEngineBugTrackingAdapter { return; } + _isInitialized = true; + try { _crashlytics = FirebaseCrashlytics.instance; - await _crashlytics!.setCrashlyticsCollectionEnabled(true); - _isInitialized = true; + await _crashlytics?.setCrashlyticsCollectionEnabled(true); } catch (e) { _isInitialized = false; debugPrint('failed to initialize Crashlytics $e'); @@ -51,7 +55,7 @@ class EngineCrashlyticsAdapter implements IEngineBugTrackingAdapter { } try { - await _crashlytics!.setCustomKey(key, value); + await _crashlytics?.setCustomKey(key, value); } catch (e) { debugPrint('setCustomKey: Error setting custom key: $e'); } @@ -69,7 +73,7 @@ class EngineCrashlyticsAdapter implements IEngineBugTrackingAdapter { } try { - await _crashlytics!.setUserIdentifier(id); + await _crashlytics?.setUserIdentifier(id); } catch (e) { debugPrint('setUserIdentifier: Error setting user identifier: $e'); } @@ -88,7 +92,7 @@ class EngineCrashlyticsAdapter implements IEngineBugTrackingAdapter { } try { - await _crashlytics!.log(message); + await _crashlytics?.log(message); } catch (e) { debugPrint('log: Error logging message: $e'); } @@ -109,7 +113,7 @@ class EngineCrashlyticsAdapter implements IEngineBugTrackingAdapter { } try { - await _crashlytics!.recordError( + await _crashlytics?.recordError( exception, stackTrace, reason: reason, @@ -130,7 +134,7 @@ class EngineCrashlyticsAdapter implements IEngineBugTrackingAdapter { } try { - await _crashlytics!.recordFlutterError(errorDetails); + await _crashlytics?.recordFlutterError(errorDetails); } catch (e) { debugPrint('recordFlutterError: Error recording Flutter error: $e'); } @@ -145,7 +149,7 @@ class EngineCrashlyticsAdapter implements IEngineBugTrackingAdapter { if (kDebugMode) { try { - _crashlytics!.crash(); + _crashlytics?.crash(); } catch (e) { debugPrint('testCrash: Error testing crash: $e'); } diff --git a/lib/src/bug_tracking/adapters/engine_faro_bug_tracking_adapter.dart b/lib/src/bug_tracking/adapters/engine_faro_bug_tracking_adapter.dart index f6914dc..ff3dd70 100644 --- a/lib/src/bug_tracking/adapters/engine_faro_bug_tracking_adapter.dart +++ b/lib/src/bug_tracking/adapters/engine_faro_bug_tracking_adapter.dart @@ -1,17 +1,17 @@ import 'dart:io'; import 'package:engine_tracking/engine_tracking.dart'; -import 'package:faro/faro_sdk.dart'; +import 'package:faro/faro.dart'; import 'package:flutter/foundation.dart'; -class EngineFaroBugTrackingAdapter implements IEngineBugTrackingAdapter { - EngineFaroBugTrackingAdapter(this._config); +class EngineFaroBugTrackingAdapter implements IEngineBugTrackingAdapter { + EngineFaroBugTrackingAdapter(this.config); @override String get adapterName => 'Grafana Faro Bug Tracking'; @override - bool get isEnabled => _config.enabled; + bool get isEnabled => config.enabled; @override bool get isInitialized => _isInitialized; @@ -19,7 +19,10 @@ class EngineFaroBugTrackingAdapter implements IEngineBugTrackingAdapter { bool get isFaroInitialized => isEnabled && _isInitialized && _faro != null; bool _isInitialized = false; - final EngineFaroConfig _config; + + @override + final EngineFaroConfig config; + Faro? _faro; @override @@ -29,9 +32,14 @@ class EngineFaroBugTrackingAdapter implements IEngineBugTrackingAdapter { return; } + _isInitialized = true; + try { _faro = Faro(); - HttpOverrides.global = FaroHttpOverrides(_config.httpOverrides); + + if (config.httpTrackingEnable) { + HttpOverrides.global = FaroHttpOverrides(config.httpOverrides ?? HttpOverrides.current); + } if (EngineAnalytics.isFaroInitialized) { return; @@ -39,18 +47,16 @@ class EngineFaroBugTrackingAdapter implements IEngineBugTrackingAdapter { await _faro?.init( optionsConfiguration: FaroConfig( - apiKey: _config.apiKey, - appName: _config.appName, - appVersion: _config.appVersion, - appEnv: _config.environment, - collectorUrl: _config.endpoint, + apiKey: config.apiKey, + appName: config.appName, + appVersion: config.appVersion, + appEnv: config.environment, + collectorUrl: config.endpoint, enableCrashReporting: true, anrTracking: true, - refreshRateVitals: true, - namespace: _config.namespace, + namespace: config.namespace, ), ); - _isInitialized = true; } catch (e) { _isInitialized = false; debugPrint('failed to initialize Faro Bug Tracking $e'); @@ -102,9 +108,9 @@ class EngineFaroBugTrackingAdapter implements IEngineBugTrackingAdapter { } try { - await _faro!.pushLog( + _faro!.pushLog( message, - level: level, + level: LogLevel.fromString(level) ?? LogLevel.info, context: convertToStringMap(attributes), trace: {'stack': (stackTrace ?? StackTrace.current).toString()}, ); @@ -134,7 +140,7 @@ class EngineFaroBugTrackingAdapter implements IEngineBugTrackingAdapter { 'isFatal': isFatal.toString(), }; - await _faro!.pushError( + _faro!.pushError( type: reason ?? 'Unknown', value: exception.toString(), context: convertToStringMap(contextData), @@ -163,7 +169,7 @@ class EngineFaroBugTrackingAdapter implements IEngineBugTrackingAdapter { 'summary': errorDetails.summary.toString(), }; - await _faro!.pushError( + _faro!.pushError( type: 'FlutterError', value: errorDetails.exception.toString(), context: convertToStringMap(contextData), @@ -185,7 +191,7 @@ class EngineFaroBugTrackingAdapter implements IEngineBugTrackingAdapter { try { final contextData = {'test': 'true'}; - await _faro!.pushError( + _faro!.pushError( type: 'TestCrash', value: 'This is a test crash for Faro', context: convertToStringMap(contextData), diff --git a/lib/src/bug_tracking/adapters/engine_google_logging_bug_tracking_adapter.dart b/lib/src/bug_tracking/adapters/engine_google_logging_bug_tracking_adapter.dart index 5b3a736..0eb6c87 100644 --- a/lib/src/bug_tracking/adapters/engine_google_logging_bug_tracking_adapter.dart +++ b/lib/src/bug_tracking/adapters/engine_google_logging_bug_tracking_adapter.dart @@ -4,8 +4,8 @@ import 'package:flutter/foundation.dart'; import 'package:googleapis/logging/v2.dart' as logging; import 'package:googleapis_auth/auth_io.dart' as auth; -class EngineGoogleLoggingBugTrackingAdapter implements IEngineBugTrackingAdapter { - EngineGoogleLoggingBugTrackingAdapter(this._config); +class EngineGoogleLoggingBugTrackingAdapter implements IEngineBugTrackingAdapter { + EngineGoogleLoggingBugTrackingAdapter(this.config); bool _isInitialized = false; @@ -13,14 +13,16 @@ class EngineGoogleLoggingBugTrackingAdapter implements IEngineBugTrackingAdapter String get adapterName => 'Google Cloud Logging Bug Tracking'; @override - bool get isEnabled => _config.enabled; + bool get isEnabled => config.enabled; @override bool get isInitialized => _isInitialized; + @override + final EngineGoogleLoggingConfig config; + bool get isGoogleLoggingBugTrackingInitialized => isEnabled && _isInitialized; - final EngineGoogleLoggingConfig _config; late final logging.LoggingApi _loggingApi; late final auth.AuthClient _authClient; final Map _customKeys = {}; @@ -34,14 +36,15 @@ class EngineGoogleLoggingBugTrackingAdapter implements IEngineBugTrackingAdapter return; } + _isInitialized = true; + try { _authClient = await auth.clientViaServiceAccount( - auth.ServiceAccountCredentials.fromJson(_config.credentials), + auth.ServiceAccountCredentials.fromJson(config.credentials), [logging.LoggingApi.loggingWriteScope], ); _loggingApi = logging.LoggingApi(_authClient); - _isInitialized = true; } catch (e) { debugPrint('initialize: Error initializing Google Cloud Logging: $e'); _isInitialized = false; @@ -123,9 +126,9 @@ class EngineGoogleLoggingBugTrackingAdapter implements IEngineBugTrackingAdapter if (stackTrace != null) 'stackTrace': stackTrace.toString(), } ..severity = level?.toUpperCase() - ..logName = 'projects/${_config.projectId}/logs/${_config.logName}-bug-tracking' + ..logName = 'projects/${config.projectId}/logs/${config.logName}-bug-tracking' ..resource = logging.MonitoredResource.fromJson( - _config.resource ?? + config.resource ?? { 'type': 'global', }, @@ -133,7 +136,7 @@ class EngineGoogleLoggingBugTrackingAdapter implements IEngineBugTrackingAdapter final request = logging.WriteLogEntriesRequest() ..entries = [logEntry] - ..logName = 'projects/${_config.projectId}/logs/${_config.logName}-bug-tracking'; + ..logName = 'projects/${config.projectId}/logs/${config.logName}-bug-tracking'; await _loggingApi.entries.write(request); } catch (e) { @@ -175,9 +178,9 @@ class EngineGoogleLoggingBugTrackingAdapter implements IEngineBugTrackingAdapter if (stackTrace != null) 'stackTrace': stackTrace.toString(), } ..severity = isFatal ? 'CRITICAL' : 'ERROR' - ..logName = 'projects/${_config.projectId}/logs/${_config.logName}-errors' + ..logName = 'projects/${config.projectId}/logs/${config.logName}-errors' ..resource = logging.MonitoredResource.fromJson( - _config.resource ?? + config.resource ?? { 'type': 'global', }, @@ -185,7 +188,7 @@ class EngineGoogleLoggingBugTrackingAdapter implements IEngineBugTrackingAdapter final request = logging.WriteLogEntriesRequest() ..entries = [logEntry] - ..logName = 'projects/${_config.projectId}/logs/${_config.logName}-errors'; + ..logName = 'projects/${config.projectId}/logs/${config.logName}-errors'; await _loggingApi.entries.write(request); } catch (e) { diff --git a/lib/src/bug_tracking/adapters/i_engine_bug_tracking_adapter.dart b/lib/src/bug_tracking/adapters/i_engine_bug_tracking_adapter.dart index 1061d46..d4287a7 100644 --- a/lib/src/bug_tracking/adapters/i_engine_bug_tracking_adapter.dart +++ b/lib/src/bug_tracking/adapters/i_engine_bug_tracking_adapter.dart @@ -1,9 +1,11 @@ +import 'package:engine_tracking/src/config/config.dart'; import 'package:flutter/foundation.dart'; -abstract class IEngineBugTrackingAdapter { +abstract class IEngineBugTrackingAdapter { String get adapterName; bool get isEnabled; bool get isInitialized; + TConfig get config; Future initialize(); Future dispose(); diff --git a/lib/src/bug_tracking/engine_bug_tracking.dart b/lib/src/bug_tracking/engine_bug_tracking.dart index 6fbebb8..f5eb5cd 100644 --- a/lib/src/bug_tracking/engine_bug_tracking.dart +++ b/lib/src/bug_tracking/engine_bug_tracking.dart @@ -22,15 +22,31 @@ class EngineBugTracking { static final _adapters = []; - static bool isAdapterInitialized(final PredicateBugTracking predicate) => _adapters.any(predicate); + static bool get isCrashlyticsInitialized => _isAdapterTypeInitialized(); + static bool get isFaroInitialized => _isAdapterTypeInitialized(); + static bool get isGoogleLoggingInitialized => _isAdapterTypeInitialized(); + + static bool _isAdapterTypeInitialized() => + _adapters.whereType().any((final adapter) => adapter.isInitialized); + + /// Retrieves the configuration for a specific adapter type. + /// + /// Returns the configuration if an enabled adapter of type [TAdapter] is found, + /// otherwise returns null. + /// + /// Example: + /// ```dart + /// final config = EngineAnalytics.getConfig(); + /// ``` + static TConfig? getConfig() { + final enabledAdapter = _adapters.whereType().where((final adapter) => adapter.isEnabled).firstOrNull; + + if (enabledAdapter?.config is TConfig) { + return enabledAdapter!.config as TConfig; + } - static bool get isCrashlyticsInitialized => - isAdapterInitialized((final adapter) => adapter is EngineCrashlyticsAdapter && adapter.isInitialized); - static bool get isFaroInitialized => - isAdapterInitialized((final adapter) => adapter is EngineFaroBugTrackingAdapter && adapter.isInitialized); - static bool get isGoogleLoggingInitialized => isAdapterInitialized( - (final adapter) => adapter is EngineGoogleLoggingBugTrackingAdapter && adapter.isInitialized, - ); + return null; + } static Future init(final List adapters) async { if (_isInitialized) { @@ -41,18 +57,17 @@ class EngineBugTracking { ..clear() ..addAll(adapters.where((final adapter) => adapter.isEnabled)); - final futures = _adapters.map((final adapter) => adapter.initialize()).toList(); - - await Future.wait(futures); + final initializes = _adapters.map((final adapter) => adapter.initialize()); + await Future.wait(initializes); _isInitialized = true; } static Future initWithModel(final EngineBugTrackingModel model) async { final adapters = [ - EngineCrashlyticsAdapter(model.crashlyticsConfig), - EngineFaroBugTrackingAdapter(model.faroConfig), - EngineGoogleLoggingBugTrackingAdapter(model.googleLoggingConfig), + if (model.crashlyticsConfig != null) EngineCrashlyticsAdapter(model.crashlyticsConfig!), + if (model.faroConfig != null) EngineFaroBugTrackingAdapter(model.faroConfig!), + if (model.googleLoggingConfig != null) EngineGoogleLoggingBugTrackingAdapter(model.googleLoggingConfig!), ]; await init(adapters); @@ -63,12 +78,8 @@ class EngineBugTracking { return; } - final futures = _adapters - .map( - (final adapter) => adapter.dispose(), - ) - .toList(); - await Future.wait(futures); + final disposes = _adapters.map((final adapter) => adapter.dispose()); + await Future.wait(disposes); _adapters.clear(); _isInitialized = false; @@ -84,12 +95,8 @@ class EngineBugTracking { return; } - final futures = _adapters - .map( - (final adapter) => adapter.setCustomKey(key, value), - ) - .toList(); - await Future.wait(futures); + final setCustomKeys = _adapters.map((final adapter) => adapter.setCustomKey(key, value)); + await Future.wait(setCustomKeys); } static Future setUserIdentifier(final String id, final String email, final String name) async { @@ -97,12 +104,8 @@ class EngineBugTracking { return; } - final futures = _adapters - .map( - (final adapter) => adapter.setUserIdentifier(id, email, name), - ) - .toList(); - await Future.wait(futures); + final setUserIdentifiers = _adapters.map((final adapter) => adapter.setUserIdentifier(id, email, name)); + await Future.wait(setUserIdentifiers); } static Future log( @@ -115,17 +118,15 @@ class EngineBugTracking { return; } - final futures = _adapters - .map( - (final adapter) => adapter.log( - message, - level: level, - attributes: attributes, - stackTrace: stackTrace, - ), - ) - .toList(); - await Future.wait(futures); + final logs = _adapters.map( + (final adapter) => adapter.log( + message, + level: level, + attributes: attributes, + stackTrace: stackTrace, + ), + ); + await Future.wait(logs); } static Future recordError( @@ -140,7 +141,7 @@ class EngineBugTracking { return; } - final futures = _adapters + final errors = _adapters .map( (final adapter) => adapter.recordError( exception, @@ -152,7 +153,7 @@ class EngineBugTracking { ), ) .toList(); - await Future.wait(futures); + await Future.wait(errors); } static Future recordFlutterError(final FlutterErrorDetails errorDetails) async { @@ -160,12 +161,12 @@ class EngineBugTracking { return; } - final futures = _adapters + final erros = _adapters .map( (final adapter) => adapter.recordFlutterError(errorDetails), ) .toList(); - await Future.wait(futures); + await Future.wait(erros); } static Future testCrash() async { diff --git a/lib/src/config/config.dart b/lib/src/config/config.dart index 1b665ed..fae8504 100644 --- a/lib/src/config/config.dart +++ b/lib/src/config/config.dart @@ -3,4 +3,6 @@ export 'engine_crashlytics_config.dart'; export 'engine_faro_config.dart'; export 'engine_firebase_analytics_config.dart'; export 'engine_google_logging_config.dart'; +export 'engine_http_tracking_config.dart'; export 'engine_splunk_config.dart'; +export 'i_engine_config.dart'; diff --git a/lib/src/config/engine_clarity_config.dart b/lib/src/config/engine_clarity_config.dart index e419ce3..d7acdbd 100644 --- a/lib/src/config/engine_clarity_config.dart +++ b/lib/src/config/engine_clarity_config.dart @@ -1,21 +1,17 @@ -import 'package:engine_tracking/src/src.dart'; +import 'package:engine_tracking/src/config/config.dart'; +import 'package:engine_tracking/src/enums/enums.dart'; -class EngineClarityConfig { - const EngineClarityConfig({ - required this.enabled, +class EngineClarityConfig extends IEngineConfig { + EngineClarityConfig({ + required super.enabled, required this.projectId, this.level = EngineLogLevelType.info, - this.userId, }); - final bool enabled; - final String projectId; - final String? userId; - final EngineLogLevelType level; @override - String toString() => 'EngineClarityConfig(enabled: $enabled, projectId: *****, userId: *****)'; + String toString() => 'EngineClarityConfig(enabled: $enabled, projectId: *****)'; } diff --git a/lib/src/config/engine_crashlytics_config.dart b/lib/src/config/engine_crashlytics_config.dart index fce227c..1b2e2fb 100644 --- a/lib/src/config/engine_crashlytics_config.dart +++ b/lib/src/config/engine_crashlytics_config.dart @@ -1,7 +1,7 @@ -class EngineCrashlyticsConfig { - const EngineCrashlyticsConfig({required this.enabled}); +import 'package:engine_tracking/src/config/config.dart'; - final bool enabled; +class EngineCrashlyticsConfig extends IEngineConfig { + EngineCrashlyticsConfig({required super.enabled}); @override String toString() => 'EngineCrashlyticsConfig(enabled: $enabled)'; diff --git a/lib/src/config/engine_faro_config.dart b/lib/src/config/engine_faro_config.dart index 34cdbd6..791caa0 100644 --- a/lib/src/config/engine_faro_config.dart +++ b/lib/src/config/engine_faro_config.dart @@ -1,8 +1,10 @@ import 'dart:io'; -class EngineFaroConfig { - const EngineFaroConfig({ - required this.enabled, +import 'package:engine_tracking/src/config/config.dart'; + +class EngineFaroConfig extends IEngineConfig { + EngineFaroConfig({ + required super.enabled, required this.endpoint, required this.appName, required this.appVersion, @@ -10,10 +12,10 @@ class EngineFaroConfig { required this.apiKey, required this.namespace, required this.platform, + this.httpTrackingEnable = false, this.httpOverrides, }); - final bool enabled; final String endpoint; final String appName; final String appVersion; @@ -22,6 +24,7 @@ class EngineFaroConfig { final String namespace; final String platform; final HttpOverrides? httpOverrides; + final bool httpTrackingEnable; @override String toString() => diff --git a/lib/src/config/engine_firebase_analytics_config.dart b/lib/src/config/engine_firebase_analytics_config.dart index 6939de0..b1d4966 100644 --- a/lib/src/config/engine_firebase_analytics_config.dart +++ b/lib/src/config/engine_firebase_analytics_config.dart @@ -1,7 +1,7 @@ -class EngineFirebaseAnalyticsConfig { - const EngineFirebaseAnalyticsConfig({required this.enabled}); +import 'package:engine_tracking/src/config/config.dart'; - final bool enabled; +class EngineFirebaseAnalyticsConfig extends IEngineConfig { + EngineFirebaseAnalyticsConfig({required super.enabled}); @override String toString() => 'EngineFirebaseAnalyticsConfig(enabled: $enabled)'; diff --git a/lib/src/config/engine_google_logging_config.dart b/lib/src/config/engine_google_logging_config.dart index 9e5d9c3..5557f87 100644 --- a/lib/src/config/engine_google_logging_config.dart +++ b/lib/src/config/engine_google_logging_config.dart @@ -1,13 +1,14 @@ -class EngineGoogleLoggingConfig { - const EngineGoogleLoggingConfig({ - required this.enabled, +import 'package:engine_tracking/src/config/config.dart'; + +class EngineGoogleLoggingConfig extends IEngineConfig { + EngineGoogleLoggingConfig({ + required super.enabled, required this.projectId, required this.logName, required this.credentials, this.resource, }); - final bool enabled; final String projectId; final String logName; final Map credentials; diff --git a/lib/src/config/engine_http_tracking_config.dart b/lib/src/config/engine_http_tracking_config.dart new file mode 100644 index 0000000..5c03994 --- /dev/null +++ b/lib/src/config/engine_http_tracking_config.dart @@ -0,0 +1,52 @@ +import 'dart:io'; + +import 'package:engine_tracking/src/config/config.dart'; + +/// Configuration class for Engine HTTP tracking +/// +/// This class defines the configuration options for HTTP request/response logging +/// using the EngineHttpOverride system. +class EngineHttpTrackingConfig extends IEngineConfig { + EngineHttpTrackingConfig({ + super.enabled = true, + this.enableRequestLogging = true, + this.enableResponseLogging = true, + this.enableTimingLogging = true, + this.enableHeaderLogging = true, + this.enableBodyLogging = true, + this.maxBodyLogLength = 1000, + this.logName = 'HTTP_TRACKING', + this.baseOverride, + }); + + /// Whether to log HTTP requests + final bool enableRequestLogging; + + /// Whether to log HTTP responses + final bool enableResponseLogging; + + /// Whether to log request/response timing + final bool enableTimingLogging; + + /// Whether to log request/response headers (be careful with sensitive data) + final bool enableHeaderLogging; + + /// Whether to log request/response body (be careful with sensitive data) + final bool enableBodyLogging; + + /// Maximum length of body content to log + final int maxBodyLogLength; + + /// Custom log name for HTTP tracking logs + final String logName; + + /// Optional base HttpOverrides to chain with (e.g., FaroHttpOverrides) + final HttpOverrides? baseOverride; + + @override + String toString() => + 'EngineHttpTrackingConfig(enabled: $enabled, enableRequestLogging: $enableRequestLogging, ' + 'enableResponseLogging: $enableResponseLogging, enableTimingLogging: $enableTimingLogging, ' + 'enableHeaderLogging: $enableHeaderLogging, enableBodyLogging: $enableBodyLogging, ' + 'maxBodyLogLength: $maxBodyLogLength, logName: $logName)'; +} diff --git a/lib/src/config/engine_splunk_config.dart b/lib/src/config/engine_splunk_config.dart index cf7c7d0..827fe02 100644 --- a/lib/src/config/engine_splunk_config.dart +++ b/lib/src/config/engine_splunk_config.dart @@ -1,6 +1,8 @@ -class EngineSplunkConfig { - const EngineSplunkConfig({ - required this.enabled, +import 'package:engine_tracking/src/config/config.dart'; + +class EngineSplunkConfig extends IEngineConfig { + EngineSplunkConfig({ + required super.enabled, required this.endpoint, required this.token, required this.source, @@ -8,7 +10,6 @@ class EngineSplunkConfig { required this.index, }); - final bool enabled; final String endpoint; final String token; final String source; diff --git a/lib/src/config/i_engine_config.dart b/lib/src/config/i_engine_config.dart new file mode 100644 index 0000000..70229eb --- /dev/null +++ b/lib/src/config/i_engine_config.dart @@ -0,0 +1,5 @@ +abstract class IEngineConfig { + IEngineConfig({required this.enabled}); + + final bool enabled; +} diff --git a/lib/src/engine_tracking_initialize.dart b/lib/src/engine_tracking_initialize.dart index 6fe253d..97547f4 100644 --- a/lib/src/engine_tracking_initialize.dart +++ b/lib/src/engine_tracking_initialize.dart @@ -9,6 +9,12 @@ import 'package:engine_tracking/engine_tracking.dart'; class EngineTrackingInitialize { EngineTrackingInitialize._(); + static void _initEngineHttp(final EngineHttpTrackingConfig? httpTrackingConfig) { + if (httpTrackingConfig != null) { + EngineHttpTracking.initialize(httpTrackingConfig, preserveExisting: true); + } + } + /// Initialize tracking services with custom adapters /// /// [analyticsAdapters] List of analytics adapters to initialize. @@ -16,9 +22,13 @@ class EngineTrackingInitialize { /// /// [bugTrackingAdapters] List of bug tracking adapters to initialize. /// Can be null to skip bug tracking initialization. + /// + /// [httpTrackingConfig] HTTP tracking configuration. + /// Can be null to skip HTTP tracking initialization. static Future initWithAdapters({ final List? analyticsAdapters, final List? bugTrackingAdapters, + final EngineHttpTrackingConfig? httpTrackingConfig, }) async { final futures = >[]; @@ -31,6 +41,8 @@ class EngineTrackingInitialize { } await Future.wait(futures); + + _initEngineHttp(httpTrackingConfig); } /// Initialize tracking services with configuration models @@ -40,9 +52,13 @@ class EngineTrackingInitialize { /// /// [bugTrackingModel] Bug tracking configuration model. /// Can be null to skip bug tracking initialization. + /// + /// [httpTrackingConfig] HTTP tracking configuration. + /// Can be null to skip HTTP tracking initialization. static Future initWithModels({ final EngineAnalyticsModel? analyticsModel, final EngineBugTrackingModel? bugTrackingModel, + final EngineHttpTrackingConfig? httpTrackingConfig, }) async { final futures = >[]; @@ -55,6 +71,8 @@ class EngineTrackingInitialize { } await Future.wait(futures); + + _initEngineHttp(httpTrackingConfig); } /// Initialize both services with default disabled configurations diff --git a/lib/src/http/engine_http_client.dart b/lib/src/http/engine_http_client.dart new file mode 100644 index 0000000..f4abc71 --- /dev/null +++ b/lib/src/http/engine_http_client.dart @@ -0,0 +1,162 @@ +import 'dart:io'; + +import 'package:engine_tracking/src/http/engine_http_client_request.dart'; + +/// Internal HTTP client wrapper that handles the actual logging +class EngineHttpClient implements HttpClient { + EngineHttpClient( + this._inner, { + required this.enableRequestLogging, + required this.enableResponseLogging, + required this.enableTimingLogging, + required this.enableHeaderLogging, + required this.enableBodyLogging, + required this.maxBodyLogLength, + required this.logName, + required this.ignoreDomains, + }); + + final HttpClient _inner; + final bool enableRequestLogging; + final bool enableResponseLogging; + final bool enableTimingLogging; + final bool enableHeaderLogging; + final bool enableBodyLogging; + final int maxBodyLogLength; + final String logName; + final List ignoreDomains; + + @override + Future open(final String method, final String host, final int port, final String path) async { + final request = await _inner.open(method, host, port, path); + return _wrapRequest(request, method, Uri(scheme: 'https', host: host, port: port, path: path)); + } + + @override + Future openUrl(final String method, final Uri url) async { + final request = await _inner.openUrl(method, url); + return _wrapRequest(request, method, url); + } + + @override + Future get(final String host, final int port, final String path) => open('GET', host, port, path); + + @override + Future getUrl(final Uri url) => openUrl('GET', url); + + @override + Future post(final String host, final int port, final String path) => + open('POST', host, port, path); + + @override + Future postUrl(final Uri url) => openUrl('POST', url); + + @override + Future put(final String host, final int port, final String path) => open('PUT', host, port, path); + + @override + Future putUrl(final Uri url) => openUrl('PUT', url); + + @override + Future delete(final String host, final int port, final String path) => + open('DELETE', host, port, path); + + @override + Future deleteUrl(final Uri url) => openUrl('DELETE', url); + + @override + Future patch(final String host, final int port, final String path) => + open('PATCH', host, port, path); + + @override + Future patchUrl(final Uri url) => openUrl('PATCH', url); + + @override + Future head(final String host, final int port, final String path) => + open('HEAD', host, port, path); + + @override + Future headUrl(final Uri url) => openUrl('HEAD', url); + + HttpClientRequest _wrapRequest(final HttpClientRequest request, final String method, final Uri uri) => + EngineHttpClientRequest( + request, + method: method, + uri: uri, + enableRequestLogging: enableRequestLogging, + enableResponseLogging: enableResponseLogging, + enableTimingLogging: enableTimingLogging, + enableHeaderLogging: enableHeaderLogging, + enableBodyLogging: enableBodyLogging, + maxBodyLogLength: maxBodyLogLength, + logName: logName, + ignoreDomains: ignoreDomains, + ); + + // Delegate all other properties and methods to the inner client + @override + Duration? get connectionTimeout => _inner.connectionTimeout; + + @override + set connectionTimeout(final Duration? value) => _inner.connectionTimeout = value; + + @override + Duration get idleTimeout => _inner.idleTimeout; + + @override + set idleTimeout(final Duration value) => _inner.idleTimeout = value; + + @override + int? get maxConnectionsPerHost => _inner.maxConnectionsPerHost; + + @override + set maxConnectionsPerHost(final int? value) => _inner.maxConnectionsPerHost = value; + + @override + bool get autoUncompress => _inner.autoUncompress; + + @override + set autoUncompress(final bool value) => _inner.autoUncompress = value; + + @override + String? get userAgent => _inner.userAgent; + + @override + set userAgent(final String? value) => _inner.userAgent = value; + + @override + void addCredentials(final Uri url, final String realm, final HttpClientCredentials credentials) => + _inner.addCredentials(url, realm, credentials); + + @override + void addProxyCredentials( + final String host, + final int port, + final String realm, + final HttpClientCredentials credentials, + ) => _inner.addProxyCredentials(host, port, realm, credentials); + + @override + set authenticate(final Future Function(Uri url, String scheme, String? realm)? f) => _inner.authenticate = f; + + @override + set authenticateProxy(final Future Function(String host, int port, String scheme, String? realm)? f) => + _inner.authenticateProxy = f; + + @override + set badCertificateCallback(final bool Function(X509Certificate cert, String host, int port)? callback) => + _inner.badCertificateCallback = callback; + + @override + set connectionFactory(final Future> Function(Uri url, String? proxyHost, int? proxyPort)? f) => + _inner.connectionFactory = f; + + @override + set keyLog(final void Function(String line)? callback) => _inner.keyLog = callback; + + @override + void close({final bool force = false}) => _inner.close(force: force); + + @override + set findProxy(final String Function(Uri url)? f) => _inner.findProxy = f; +} diff --git a/lib/src/http/engine_http_client_request.dart b/lib/src/http/engine_http_client_request.dart new file mode 100644 index 0000000..4e99e71 --- /dev/null +++ b/lib/src/http/engine_http_client_request.dart @@ -0,0 +1,212 @@ +import 'dart:async'; +import 'dart:convert'; +import 'dart:io'; + +import 'package:engine_tracking/engine_tracking.dart'; + +/// Internal HTTP request wrapper that handles logging +class EngineHttpClientRequest implements HttpClientRequest { + EngineHttpClientRequest( + this._inner, { + required this.method, + required this.uri, + required this.enableRequestLogging, + required this.enableResponseLogging, + required this.enableTimingLogging, + required this.enableHeaderLogging, + required this.enableBodyLogging, + required this.maxBodyLogLength, + required this.logName, + required this.ignoreDomains, + }) { + _startTime = DateTime.now(); + unawaited(_logRequest()); + } + + final HttpClientRequest _inner; + @override + final String method; + @override + final Uri uri; + final bool enableRequestLogging; + final bool enableResponseLogging; + final bool enableTimingLogging; + final bool enableHeaderLogging; + final bool enableBodyLogging; + final int maxBodyLogLength; + final String logName; + final List ignoreDomains; + + late final DateTime _startTime; + final List _requestBody = []; + + Future _logRequest() async { + if (!enableRequestLogging) return; + + if (ignoreDomains.any((final item) => uri.host.contains(item))) return; + + final requestData = { + 'method': method, + 'url': uri.toString(), + 'host': uri.host, + 'path': uri.path, + 'timestamp': _startTime.toIso8601String(), + }; + + if (enableHeaderLogging) { + final headersMap = {}; + _inner.headers.forEach((final String name, final List values) { + headersMap[name] = values.join(', '); + }); + requestData['headers'] = headersMap; + } + + await EngineLog.debug('HTTP Request Started', logName: logName, data: requestData); + } + + Future _logResponse(final HttpClientResponse response) async { + if (!enableResponseLogging) return; + + if (ignoreDomains.any((final item) => uri.host.contains(item))) return; + + final endTime = DateTime.now(); + final duration = endTime.difference(_startTime); + + final responseData = { + 'method': method, + 'url': uri.toString(), + 'status_code': response.statusCode, + 'reason_phrase': response.reasonPhrase, + 'content_length': response.contentLength, + 'timestamp': endTime.toIso8601String(), + }; + + if (enableTimingLogging) { + responseData['duration_ms'] = duration.inMilliseconds; + responseData['start_time'] = _startTime.toIso8601String(); + responseData['end_time'] = endTime.toIso8601String(); + } + + if (enableHeaderLogging) { + final responseHeadersMap = {}; + response.headers.forEach((final String name, final List values) { + responseHeadersMap[name] = values.join(', '); + }); + responseData['response_headers'] = responseHeadersMap; + } + + if (enableBodyLogging && _requestBody.isNotEmpty) { + final bodyString = String.fromCharCodes(_requestBody); + responseData['request_body'] = bodyString.length > maxBodyLogLength + ? '${bodyString.substring(0, maxBodyLogLength)}...[truncated]' + : bodyString; + } + + final message = response.statusCode >= 400 ? 'HTTP Request Failed' : 'HTTP Request Completed'; + + if (response.statusCode >= 400) { + await EngineLog.error(message, logName: logName, data: responseData); + } else { + await EngineLog.debug(message, logName: logName, data: responseData); + } + } + + @override + Future close() async { + final response = await _inner.close(); + await _logResponse(response); + return response; + } + + @override + void add(final List data) { + if (enableBodyLogging) { + _requestBody.addAll(data); + } + _inner.add(data); + } + + @override + void addError(final Object error, [final StackTrace? stackTrace]) { + unawaited( + EngineLog.error( + 'HTTP Request Error', + logName: logName, + error: error, + stackTrace: stackTrace, + data: {'method': method, 'url': uri.toString(), 'timestamp': DateTime.now().toIso8601String()}, + ), + ); + _inner.addError(error, stackTrace); + } + + // Delegate all other properties and methods to the inner request + @override + Encoding get encoding => _inner.encoding; + + @override + set encoding(final Encoding value) => _inner.encoding = value; + + @override + HttpHeaders get headers => _inner.headers; + + @override + HttpConnectionInfo? get connectionInfo => _inner.connectionInfo; + + @override + List get cookies => _inner.cookies; + + @override + Future get done => _inner.done; + + @override + bool get followRedirects => _inner.followRedirects; + + @override + set followRedirects(final bool value) => _inner.followRedirects = value; + + @override + int get maxRedirects => _inner.maxRedirects; + + @override + set maxRedirects(final int value) => _inner.maxRedirects = value; + + @override + bool get persistentConnection => _inner.persistentConnection; + + @override + set persistentConnection(final bool value) => _inner.persistentConnection = value; + + @override + Future flush() => _inner.flush(); + + @override + void write(final Object? object) => _inner.write(object); + + @override + void writeAll(final Iterable objects, [final String separator = '']) => _inner.writeAll(objects, separator); + + @override + void writeCharCode(final int charCode) => _inner.writeCharCode(charCode); + + @override + void writeln([final Object? object = '']) => _inner.writeln(object); + + @override + bool get bufferOutput => _inner.bufferOutput; + + @override + set bufferOutput(final bool value) => _inner.bufferOutput = value; + + @override + int get contentLength => _inner.contentLength; + + @override + set contentLength(final int value) => _inner.contentLength = value; + + @override + void abort([final Object? exception, final StackTrace? stackTrace]) => _inner.abort(exception, stackTrace); + + @override + Future addStream(final Stream> stream) => _inner.addStream(stream); +} diff --git a/lib/src/http/engine_http_override.dart b/lib/src/http/engine_http_override.dart new file mode 100644 index 0000000..1fdaf8d --- /dev/null +++ b/lib/src/http/engine_http_override.dart @@ -0,0 +1,65 @@ +import 'dart:io'; + +import 'package:engine_tracking/src/http/engine_http_client.dart'; + +/// Custom HTTP override that logs HTTP requests using EngineLog.debug +/// +/// This class extends HttpOverrides to intercept HTTP requests and log them +/// using the Engine Tracking logging system. It provides detailed logging +/// of request/response data including timing, status codes, and headers. +class EngineHttpOverride extends HttpOverrides { + EngineHttpOverride({ + this.enableRequestLogging = true, + this.enableResponseLogging = true, + this.enableTimingLogging = true, + this.enableHeaderLogging = false, + this.enableBodyLogging = false, + this.maxBodyLogLength = 1000, + this.logName = 'HTTP_TRACKING', + this.ignoreDomains = const ['grafana', 'datadog', 'crashlytics', 'analytics', 'firebase', 'clarity'], + this.existingOverrides, + }); + + /// Whether to log HTTP requests + final bool enableRequestLogging; + + /// Whether to log HTTP responses + final bool enableResponseLogging; + + /// Whether to log request/response timing + final bool enableTimingLogging; + + /// Whether to log request/response headers + final bool enableHeaderLogging; + + /// Whether to log request/response body (be careful with sensitive data) + final bool enableBodyLogging; + + /// Maximum length of body content to log + final int maxBodyLogLength; + + /// Custom log name for HTTP tracking logs + final String logName; + + /// Optional existing Overrides to chain with + final HttpOverrides? existingOverrides; + + /// Ignore domain registered in list + final List ignoreDomains; + + @override + HttpClient createHttpClient(final SecurityContext? context) { + final client = existingOverrides?.createHttpClient(context) ?? super.createHttpClient(context); + return EngineHttpClient( + client, + enableRequestLogging: enableRequestLogging, + enableResponseLogging: enableResponseLogging, + enableTimingLogging: enableTimingLogging, + enableHeaderLogging: enableHeaderLogging, + enableBodyLogging: enableBodyLogging, + maxBodyLogLength: maxBodyLogLength, + logName: logName, + ignoreDomains: ignoreDomains, + ); + } +} diff --git a/lib/src/http/engine_http_tracking.dart b/lib/src/http/engine_http_tracking.dart new file mode 100644 index 0000000..79f11a2 --- /dev/null +++ b/lib/src/http/engine_http_tracking.dart @@ -0,0 +1,144 @@ +import 'dart:async'; +import 'dart:io'; + +import 'package:engine_tracking/engine_tracking.dart'; +import 'package:engine_tracking/src/http/engine_http_override.dart'; + +/// Utility class for managing Engine HTTP tracking +/// +/// This class provides static methods to enable/disable HTTP tracking +/// and manage the global HttpOverrides configuration. +class EngineHttpTracking { + static EngineHttpTrackingConfig? _config; + static HttpOverrides? _previousOverride; + static bool _isEnabled = false; + + /// Gets the current HTTP tracking configuration + static EngineHttpTrackingConfig? get config => _config; + + /// Whether HTTP tracking is currently enabled + static bool get isEnabled => _isEnabled; + + /// Initializes HTTP tracking with the given configuration + /// + /// This method sets up the global HttpOverrides to use EngineHttpOverride + /// for logging HTTP requests and responses. + /// + /// [config] The configuration for HTTP tracking + /// [preserveExisting] Whether to preserve existing HttpOverrides by chaining them + static void initialize(final EngineHttpTrackingConfig config, {final bool preserveExisting = true}) { + if (!config.enabled) { + disable(); + return; + } + + _config = config; + + HttpOverrides? baseOverride = config.baseOverride; + if (preserveExisting && baseOverride == null) { + baseOverride = HttpOverrides.current; + } + + final engineOverride = EngineHttpOverride( + enableRequestLogging: config.enableRequestLogging, + enableResponseLogging: config.enableResponseLogging, + enableTimingLogging: config.enableTimingLogging, + enableHeaderLogging: config.enableHeaderLogging, + enableBodyLogging: config.enableBodyLogging, + maxBodyLogLength: config.maxBodyLogLength, + logName: config.logName, + existingOverrides: baseOverride, + ); + + // Set the global override + HttpOverrides.global = engineOverride; + _isEnabled = true; + + unawaited( + EngineLog.info( + 'HTTP tracking initialized', + logName: 'ENGINE_HTTP_TRACKING', + data: { + 'config': config.toString(), + 'has_base_override': baseOverride != null, + 'preserve_existing': preserveExisting, + }, + ), + ); + } + + /// Disables HTTP tracking + /// + /// This method removes the EngineHttpOverride and optionally restores + /// the previous HttpOverrides configuration. + static void disable() { + if (!_isEnabled) return; + + // Restore previous override if it exists + if (_previousOverride != null) { + HttpOverrides.global = _previousOverride; + _previousOverride = null; + } else { + HttpOverrides.global = null; + } + + _isEnabled = false; + _config = null; + + unawaited(EngineLog.info('HTTP tracking disabled', logName: 'ENGINE_HTTP_TRACKING')); + } + + /// Updates the HTTP tracking configuration + /// + /// This method allows you to update the configuration without fully + /// reinitializing the system. + static void updateConfig(final EngineHttpTrackingConfig newConfig) { + if (_config == null) { + initialize(newConfig); + return; + } + + final preserveExisting = HttpOverrides.current != null; + initialize(newConfig, preserveExisting: preserveExisting); + } + + /// Creates a scoped HTTP tracking configuration + /// + /// This method temporarily applies a different configuration for the + /// duration of the provided function execution. + static Future withConfig(final EngineHttpTrackingConfig config, final Future Function() operation) async { + final previousConfig = _config; + final wasEnabled = _isEnabled; + + try { + initialize(config); + return await operation(); + } finally { + if (wasEnabled && previousConfig != null) { + initialize(previousConfig); + } else { + disable(); + } + } + } + + /// Logs a custom HTTP-related event + /// + /// This method can be used to log custom HTTP-related events that + /// are not automatically captured by the HttpOverride. + static Future logCustomEvent( + final String message, { + final Map? data, + final String? logName, + }) async { + await EngineLog.debug(message, logName: logName ?? _config?.logName ?? 'HTTP_TRACKING', data: data); + } + + /// Gets statistics about HTTP tracking + static Map getStats() => { + 'is_enabled': _isEnabled, + 'has_config': _config != null, + 'config': _config?.toString(), + 'current_override': HttpOverrides.current?.runtimeType.toString(), + }; +} diff --git a/lib/src/logging/engine_log.dart b/lib/src/logging/engine_log.dart index 80bc4ae..dde626f 100644 --- a/lib/src/logging/engine_log.dart +++ b/lib/src/logging/engine_log.dart @@ -22,11 +22,12 @@ class EngineLog { final levelLog = level ?? EngineLogLevelType.info; final prefix = _getLevelPrefix(levelLog); final dataString = data == null ? '' : '- [Data]: ${data.toString()}'; + final time = DateTime.now().toIso8601String(); - final logMessage = '$prefix $message $dataString'; + final logMessage = '$prefix $message'; developer.log( - logMessage, + logMessage + dataString, name: logName, error: error, stackTrace: stackTrace, @@ -39,27 +40,17 @@ class EngineLog { 'tag': logName, 'level': levelLog.name, if (data != null) ...data, - 'time': DateTime.now(), + 'time': time, }; final attributes = EngineSession.instance.enrichWithSessionId(baseAttributes); if (EngineAnalytics.isEnabled && includeInAnalytics) { - await EngineAnalytics.logEvent( - message, - attributes, - ); + await EngineAnalytics.logEvent(message, attributes); } if (EngineBugTracking.isEnabled) { - unawaited( - EngineBugTracking.log( - message, - attributes: attributes, - level: levelLog.name, - stackTrace: stackTrace, - ), - ); + unawaited(EngineBugTracking.log(message, attributes: attributes, level: levelLog.name, stackTrace: stackTrace)); if (levelLog == EngineLogLevelType.error || levelLog == EngineLogLevelType.fatal) { await EngineBugTracking.recordError( diff --git a/lib/src/models/engine_analytics_model.dart b/lib/src/models/engine_analytics_model.dart index 5015e41..b24f8f7 100644 --- a/lib/src/models/engine_analytics_model.dart +++ b/lib/src/models/engine_analytics_model.dart @@ -13,11 +13,11 @@ class EngineAnalyticsModel { required this.splunkConfig, }); - final EngineClarityConfig clarityConfig; - final EngineFirebaseAnalyticsConfig firebaseAnalyticsConfig; - final EngineFaroConfig faroConfig; - final EngineGoogleLoggingConfig googleLoggingConfig; - final EngineSplunkConfig splunkConfig; + final EngineClarityConfig? clarityConfig; + final EngineFirebaseAnalyticsConfig? firebaseAnalyticsConfig; + final EngineFaroConfig? faroConfig; + final EngineGoogleLoggingConfig? googleLoggingConfig; + final EngineSplunkConfig? splunkConfig; @override String toString() => @@ -26,13 +26,13 @@ class EngineAnalyticsModel { class EngineAnalyticsModelDefault implements EngineAnalyticsModel { @override - EngineClarityConfig get clarityConfig => const EngineClarityConfig(enabled: false, projectId: ''); + EngineClarityConfig get clarityConfig => EngineClarityConfig(enabled: false, projectId: ''); @override - EngineFirebaseAnalyticsConfig get firebaseAnalyticsConfig => const EngineFirebaseAnalyticsConfig(enabled: false); + EngineFirebaseAnalyticsConfig get firebaseAnalyticsConfig => EngineFirebaseAnalyticsConfig(enabled: false); @override - EngineFaroConfig get faroConfig => const EngineFaroConfig( + EngineFaroConfig get faroConfig => EngineFaroConfig( enabled: false, endpoint: '', appName: '', @@ -44,14 +44,10 @@ class EngineAnalyticsModelDefault implements EngineAnalyticsModel { ); @override - EngineGoogleLoggingConfig get googleLoggingConfig => const EngineGoogleLoggingConfig( - enabled: false, - projectId: '', - logName: '', - credentials: {}, - ); + EngineGoogleLoggingConfig get googleLoggingConfig => + EngineGoogleLoggingConfig(enabled: false, projectId: '', logName: '', credentials: {}); @override EngineSplunkConfig get splunkConfig => - const EngineSplunkConfig(enabled: false, endpoint: '', token: '', source: '', sourcetype: '', index: ''); + EngineSplunkConfig(enabled: false, endpoint: '', token: '', source: '', sourcetype: '', index: ''); } diff --git a/lib/src/models/engine_bug_tracking_model.dart b/lib/src/models/engine_bug_tracking_model.dart index c3f5ec6..2923e7f 100644 --- a/lib/src/models/engine_bug_tracking_model.dart +++ b/lib/src/models/engine_bug_tracking_model.dart @@ -3,15 +3,11 @@ import 'package:engine_tracking/src/config/engine_faro_config.dart'; import 'package:engine_tracking/src/config/engine_google_logging_config.dart'; class EngineBugTrackingModel { - EngineBugTrackingModel({ - required this.crashlyticsConfig, - required this.faroConfig, - required this.googleLoggingConfig, - }); + EngineBugTrackingModel({this.crashlyticsConfig, this.faroConfig, this.googleLoggingConfig}); - final EngineCrashlyticsConfig crashlyticsConfig; - final EngineFaroConfig faroConfig; - final EngineGoogleLoggingConfig googleLoggingConfig; + final EngineCrashlyticsConfig? crashlyticsConfig; + final EngineFaroConfig? faroConfig; + final EngineGoogleLoggingConfig? googleLoggingConfig; @override String toString() => @@ -20,10 +16,10 @@ class EngineBugTrackingModel { class EngineBugTrackingModelDefault implements EngineBugTrackingModel { @override - EngineCrashlyticsConfig get crashlyticsConfig => const EngineCrashlyticsConfig(enabled: false); + EngineCrashlyticsConfig get crashlyticsConfig => EngineCrashlyticsConfig(enabled: false); @override - EngineFaroConfig get faroConfig => const EngineFaroConfig( + EngineFaroConfig get faroConfig => EngineFaroConfig( enabled: false, endpoint: '', appName: '', @@ -35,10 +31,6 @@ class EngineBugTrackingModelDefault implements EngineBugTrackingModel { ); @override - EngineGoogleLoggingConfig get googleLoggingConfig => const EngineGoogleLoggingConfig( - enabled: false, - projectId: '', - logName: '', - credentials: {}, - ); + EngineGoogleLoggingConfig get googleLoggingConfig => + EngineGoogleLoggingConfig(enabled: false, projectId: '', logName: '', credentials: {}); } diff --git a/lib/src/observers/engine_navigator_observer.dart b/lib/src/observers/engine_navigator_observer.dart index c837859..915112e 100644 --- a/lib/src/observers/engine_navigator_observer.dart +++ b/lib/src/observers/engine_navigator_observer.dart @@ -6,24 +6,18 @@ class EngineNavigationObserver extends RouteObserver> { @override Future didPop(final Route route, final Route? previousRoute) async { super.didPop(route, previousRoute); - await EngineAnalytics.setPage( - previousRoute?.settings.name ?? rootRouteName, - route.settings.name ?? rootRouteName, - ); + await EngineAnalytics.setPage(previousRoute?.settings.name ?? rootRouteName, route.settings.name ?? rootRouteName); } @override Future didPush(final Route route, final Route? previousRoute) async { super.didPush(route, previousRoute); - await EngineAnalytics.setPage(route.settings.name ?? rootRouteName, previousRoute?.settings.name ?? rootRouteName); + await EngineAnalytics.setPage(route.settings.name ?? rootRouteName, previousRoute?.settings.name); } @override Future didReplace({final Route? newRoute, final Route? oldRoute}) async { super.didReplace(newRoute: newRoute, oldRoute: oldRoute); - await EngineAnalytics.setPage( - oldRoute?.settings.name ?? rootRouteName, - newRoute?.settings.name ?? rootRouteName, - ); + await EngineAnalytics.setPage(oldRoute?.settings.name ?? rootRouteName, newRoute?.settings.name ?? rootRouteName); } } diff --git a/lib/src/src.dart b/lib/src/src.dart index 9ef9bb2..3ffc89f 100644 --- a/lib/src/src.dart +++ b/lib/src/src.dart @@ -3,6 +3,7 @@ export 'bug_tracking/bug_tracking.dart'; export 'config/config.dart'; export 'engine_tracking_initialize.dart'; export 'enums/enums.dart'; +export 'http/engine_http_tracking.dart'; export 'logging/logging.dart'; export 'models/models.dart'; export 'observers/observers.dart'; diff --git a/lib/src/widgets/engine_widget.dart b/lib/src/widgets/engine_widget.dart index 76bc6d1..ed74c8c 100644 --- a/lib/src/widgets/engine_widget.dart +++ b/lib/src/widgets/engine_widget.dart @@ -2,36 +2,81 @@ import 'package:clarity_flutter/clarity_flutter.dart'; import 'package:engine_tracking/engine_tracking.dart'; import 'package:flutter/material.dart'; +/// Engine Widget that automatically integrates with initialized tracking services +/// +/// This widget automatically detects if Microsoft Clarity has been initialized +/// through the EngineAnalytics system and wraps the app with ClarityWidget if needed. +/// No manual configuration is required - it uses the configuration from the +/// initialized Clarity adapter. +/// +/// The widget will: +/// 1. Check if EngineAnalytics has been initialized with a Clarity adapter +/// 2. If Clarity is enabled and configured, wrap the app with ClarityWidget +/// 3. If Clarity is not available or disabled, return the app unchanged +/// +/// Example usage: +/// ```dart +/// void main() async { +/// WidgetsFlutterBinding.ensureInitialized(); +/// +/// // Initialize analytics with Clarity configuration +/// final analyticsModel = EngineAnalyticsModel( +/// clarityConfig: EngineClarityConfig( +/// enabled: true, +/// projectId: 'your-project-id', +/// ), +/// // ... other configs +/// ); +/// +/// await EngineAnalytics.initWithModel(analyticsModel); +/// +/// // EngineWidget will automatically detect and use Clarity if is initialize +/// runApp(EngineWidget(app: MyApp())); +/// } +/// ``` class EngineWidget extends StatelessWidget { const EngineWidget({ required this.app, - required this.clarityConfig, super.key, }); + /// The main application widget to wrap final Widget app; - final EngineClarityConfig clarityConfig; @override Widget build(final BuildContext context) { - if (clarityConfig.enabled) { + // Check if Clarity has been initialized through EngineAnalytics + final clarityConfig = EngineAnalytics.getConfig(); + + if (clarityConfig != null && clarityConfig.enabled) { return ClarityWidget( app: app, clarityConfig: ClarityConfig( projectId: clarityConfig.projectId, - userId: clarityConfig.userId, - logLevel: switch (clarityConfig.level) { - EngineLogLevelType.verbose => LogLevel.Verbose, - EngineLogLevelType.fatal => LogLevel.Verbose, - EngineLogLevelType.debug => LogLevel.Debug, - EngineLogLevelType.info => LogLevel.Info, - EngineLogLevelType.warning => LogLevel.Warn, - EngineLogLevelType.error => LogLevel.Error, - EngineLogLevelType.none => LogLevel.None, - }, + + logLevel: _mapLogLevel(clarityConfig.level), ), ); } + return app; } + + LogLevel _mapLogLevel(final EngineLogLevelType level) { + switch (level) { + case EngineLogLevelType.verbose: + case EngineLogLevelType.fatal: + return LogLevel.Verbose; + case EngineLogLevelType.debug: + return LogLevel.Debug; + case EngineLogLevelType.info: + return LogLevel.Info; + case EngineLogLevelType.warning: + return LogLevel.Warn; + case EngineLogLevelType.error: + return LogLevel.Error; + case EngineLogLevelType.none: + return LogLevel.None; + } + } } diff --git a/pubspec.yaml b/pubspec.yaml index 1b3cb1e..d9a069b 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -31,17 +31,16 @@ environment: dependencies: flutter: sdk: flutter - firebase_crashlytics: ^4.3.7 - firebase_analytics: ^11.5.0 - faro: ^0.3.6 + firebase_crashlytics: ^4.3.10 + firebase_analytics: ^11.6.0 + faro: ^0.4.1 googleapis: ^14.0.0 googleapis_auth: ^2.0.0 - clarity_flutter: ^1.0.0 + clarity_flutter: ^1.2.0 dev_dependencies: flutter_test: sdk: flutter flutter_lints: ^6.0.0 - mockito: ^5.4.4 - build_runner: ^2.4.9 - http: ^1.2.0 + mockito: ^5.5.0 + build_runner: ^2.6.0 diff --git a/test/analytics/engine_analytics_test.dart b/test/analytics/engine_analytics_test.dart index d3c3be5..9d2470f 100644 --- a/test/analytics/engine_analytics_test.dart +++ b/test/analytics/engine_analytics_test.dart @@ -10,12 +10,12 @@ void main() { }); test('should initialize with adapters', () async { - final adapters = [ + final adapters = [ EngineFirebaseAnalyticsAdapter( - const EngineFirebaseAnalyticsConfig(enabled: false), + EngineFirebaseAnalyticsConfig(enabled: false), ), EngineFaroAnalyticsAdapter( - const EngineFaroConfig( + EngineFaroConfig( enabled: false, endpoint: '', appName: '', @@ -27,7 +27,7 @@ void main() { ), ), EngineSplunkAnalyticsAdapter( - const EngineSplunkConfig( + EngineSplunkConfig( enabled: false, endpoint: '', token: '', @@ -46,8 +46,8 @@ void main() { test('should initialize with model', () async { final model = EngineAnalyticsModel( - firebaseAnalyticsConfig: const EngineFirebaseAnalyticsConfig(enabled: false), - faroConfig: const EngineFaroConfig( + firebaseAnalyticsConfig: EngineFirebaseAnalyticsConfig(enabled: false), + faroConfig: EngineFaroConfig( enabled: false, endpoint: '', appName: '', @@ -58,7 +58,7 @@ void main() { platform: '', ), googleLoggingConfig: TestConfigs.googleLoggingConfig, - splunkConfig: const EngineSplunkConfig( + splunkConfig: EngineSplunkConfig( enabled: false, endpoint: '', token: '', @@ -66,7 +66,7 @@ void main() { sourcetype: '', index: '', ), - clarityConfig: const EngineClarityConfig( + clarityConfig: EngineClarityConfig( enabled: false, projectId: '', ), @@ -80,8 +80,8 @@ void main() { test('should handle log event without throwing', () async { final model = EngineAnalyticsModel( - firebaseAnalyticsConfig: const EngineFirebaseAnalyticsConfig(enabled: false), - faroConfig: const EngineFaroConfig( + firebaseAnalyticsConfig: EngineFirebaseAnalyticsConfig(enabled: false), + faroConfig: EngineFaroConfig( enabled: false, endpoint: '', appName: '', @@ -92,7 +92,7 @@ void main() { platform: '', ), googleLoggingConfig: TestConfigs.googleLoggingConfig, - splunkConfig: const EngineSplunkConfig( + splunkConfig: EngineSplunkConfig( enabled: false, endpoint: '', token: '', @@ -100,7 +100,7 @@ void main() { sourcetype: '', index: '', ), - clarityConfig: const EngineClarityConfig( + clarityConfig: EngineClarityConfig( enabled: false, projectId: '', ), @@ -113,8 +113,8 @@ void main() { test('should handle setUserId without throwing', () async { final model = EngineAnalyticsModel( - firebaseAnalyticsConfig: const EngineFirebaseAnalyticsConfig(enabled: false), - faroConfig: const EngineFaroConfig( + firebaseAnalyticsConfig: EngineFirebaseAnalyticsConfig(enabled: false), + faroConfig: EngineFaroConfig( enabled: false, endpoint: '', appName: '', @@ -125,7 +125,7 @@ void main() { platform: '', ), googleLoggingConfig: TestConfigs.googleLoggingConfig, - splunkConfig: const EngineSplunkConfig( + splunkConfig: EngineSplunkConfig( enabled: false, endpoint: '', token: '', @@ -133,7 +133,7 @@ void main() { sourcetype: '', index: '', ), - clarityConfig: const EngineClarityConfig( + clarityConfig: EngineClarityConfig( enabled: false, projectId: '', ), @@ -146,8 +146,8 @@ void main() { test('should handle setUserProperty without throwing', () async { final model = EngineAnalyticsModel( - firebaseAnalyticsConfig: const EngineFirebaseAnalyticsConfig(enabled: false), - faroConfig: const EngineFaroConfig( + firebaseAnalyticsConfig: EngineFirebaseAnalyticsConfig(enabled: false), + faroConfig: EngineFaroConfig( enabled: false, endpoint: '', appName: '', @@ -158,7 +158,7 @@ void main() { platform: '', ), googleLoggingConfig: TestConfigs.googleLoggingConfig, - splunkConfig: const EngineSplunkConfig( + splunkConfig: EngineSplunkConfig( enabled: false, endpoint: '', token: '', @@ -166,7 +166,7 @@ void main() { sourcetype: '', index: '', ), - clarityConfig: const EngineClarityConfig( + clarityConfig: EngineClarityConfig( enabled: false, projectId: '', ), @@ -179,8 +179,8 @@ void main() { test('should handle setPage without throwing', () async { final model = EngineAnalyticsModel( - firebaseAnalyticsConfig: const EngineFirebaseAnalyticsConfig(enabled: false), - faroConfig: const EngineFaroConfig( + firebaseAnalyticsConfig: EngineFirebaseAnalyticsConfig(enabled: false), + faroConfig: EngineFaroConfig( enabled: false, endpoint: '', appName: '', @@ -191,7 +191,7 @@ void main() { platform: '', ), googleLoggingConfig: TestConfigs.googleLoggingConfig, - splunkConfig: const EngineSplunkConfig( + splunkConfig: EngineSplunkConfig( enabled: false, endpoint: '', token: '', @@ -199,7 +199,7 @@ void main() { sourcetype: '', index: '', ), - clarityConfig: const EngineClarityConfig( + clarityConfig: EngineClarityConfig( enabled: false, projectId: '', ), @@ -212,8 +212,8 @@ void main() { test('should handle logAppOpen without throwing', () async { final model = EngineAnalyticsModel( - firebaseAnalyticsConfig: const EngineFirebaseAnalyticsConfig(enabled: false), - faroConfig: const EngineFaroConfig( + firebaseAnalyticsConfig: EngineFirebaseAnalyticsConfig(enabled: false), + faroConfig: EngineFaroConfig( enabled: false, endpoint: '', appName: '', @@ -224,7 +224,7 @@ void main() { platform: '', ), googleLoggingConfig: TestConfigs.googleLoggingConfig, - splunkConfig: const EngineSplunkConfig( + splunkConfig: EngineSplunkConfig( enabled: false, endpoint: '', token: '', @@ -232,7 +232,7 @@ void main() { sourcetype: '', index: '', ), - clarityConfig: const EngineClarityConfig( + clarityConfig: EngineClarityConfig( enabled: false, projectId: '', ), @@ -245,8 +245,8 @@ void main() { test('should have enabled adapters when configs are enabled', () async { final model = EngineAnalyticsModel( - firebaseAnalyticsConfig: const EngineFirebaseAnalyticsConfig(enabled: true), - faroConfig: const EngineFaroConfig( + firebaseAnalyticsConfig: EngineFirebaseAnalyticsConfig(enabled: true), + faroConfig: EngineFaroConfig( enabled: true, endpoint: 'https://example.com', appName: 'TestApp', @@ -257,7 +257,7 @@ void main() { platform: '', ), googleLoggingConfig: TestConfigs.googleLoggingConfig, - splunkConfig: const EngineSplunkConfig( + splunkConfig: EngineSplunkConfig( enabled: false, endpoint: '', token: '', @@ -265,7 +265,7 @@ void main() { sourcetype: '', index: '', ), - clarityConfig: const EngineClarityConfig( + clarityConfig: EngineClarityConfig( enabled: false, projectId: '', ), @@ -279,8 +279,8 @@ void main() { test('should handle reset without throwing', () async { final model = EngineAnalyticsModel( - firebaseAnalyticsConfig: const EngineFirebaseAnalyticsConfig(enabled: false), - faroConfig: const EngineFaroConfig( + firebaseAnalyticsConfig: EngineFirebaseAnalyticsConfig(enabled: false), + faroConfig: EngineFaroConfig( enabled: false, endpoint: '', appName: '', @@ -291,7 +291,7 @@ void main() { platform: '', ), googleLoggingConfig: TestConfigs.googleLoggingConfig, - splunkConfig: const EngineSplunkConfig( + splunkConfig: EngineSplunkConfig( enabled: false, endpoint: '', token: '', @@ -299,7 +299,7 @@ void main() { sourcetype: '', index: '', ), - clarityConfig: const EngineClarityConfig( + clarityConfig: EngineClarityConfig( enabled: false, projectId: '', ), @@ -312,8 +312,8 @@ void main() { test('should handle custom events', () async { final model = EngineAnalyticsModel( - firebaseAnalyticsConfig: const EngineFirebaseAnalyticsConfig(enabled: false), - faroConfig: const EngineFaroConfig( + firebaseAnalyticsConfig: EngineFirebaseAnalyticsConfig(enabled: false), + faroConfig: EngineFaroConfig( enabled: false, endpoint: '', appName: '', @@ -324,7 +324,7 @@ void main() { platform: '', ), googleLoggingConfig: TestConfigs.googleLoggingConfig, - splunkConfig: const EngineSplunkConfig( + splunkConfig: EngineSplunkConfig( enabled: false, endpoint: '', token: '', @@ -332,7 +332,7 @@ void main() { sourcetype: '', index: '', ), - clarityConfig: const EngineClarityConfig( + clarityConfig: EngineClarityConfig( enabled: false, projectId: '', ), diff --git a/test/bug_tracking/engine_bug_tracking_test.dart b/test/bug_tracking/engine_bug_tracking_test.dart index 203391b..5486cf2 100644 --- a/test/bug_tracking/engine_bug_tracking_test.dart +++ b/test/bug_tracking/engine_bug_tracking_test.dart @@ -16,8 +16,8 @@ void main() { test('should handle disabled services', () async { final model = EngineBugTrackingModel( - crashlyticsConfig: const EngineCrashlyticsConfig(enabled: false), - faroConfig: const EngineFaroConfig( + crashlyticsConfig: EngineCrashlyticsConfig(enabled: false), + faroConfig: EngineFaroConfig( enabled: false, endpoint: '', appName: '', @@ -37,12 +37,12 @@ void main() { }); test('should initialize with adapters', () async { - final adapters = [ + final adapters = [ EngineCrashlyticsAdapter( - const EngineCrashlyticsConfig(enabled: false), + EngineCrashlyticsConfig(enabled: false), ), EngineFaroBugTrackingAdapter( - const EngineFaroConfig( + EngineFaroConfig( enabled: false, endpoint: '', appName: '', @@ -63,8 +63,8 @@ void main() { test('should initialize with Crashlytics enabled only (mocked)', () async { final model = EngineBugTrackingModel( - crashlyticsConfig: const EngineCrashlyticsConfig(enabled: true), - faroConfig: const EngineFaroConfig( + crashlyticsConfig: EngineCrashlyticsConfig(enabled: true), + faroConfig: EngineFaroConfig( enabled: false, endpoint: '', appName: '', @@ -77,14 +77,14 @@ void main() { googleLoggingConfig: TestConfigs.googleLoggingConfig, ); - expect(model.crashlyticsConfig.enabled, isTrue); - expect(model.faroConfig.enabled, isFalse); + expect(model.crashlyticsConfig?.enabled, isTrue); + expect(model.faroConfig?.enabled, isFalse); }); test('should initialize with Faro enabled only (mocked)', () async { final model = EngineBugTrackingModel( - crashlyticsConfig: const EngineCrashlyticsConfig(enabled: false), - faroConfig: const EngineFaroConfig( + crashlyticsConfig: EngineCrashlyticsConfig(enabled: false), + faroConfig: EngineFaroConfig( enabled: true, endpoint: 'https://faro.example.com', appName: 'TestApp', @@ -97,15 +97,15 @@ void main() { googleLoggingConfig: TestConfigs.googleLoggingConfig, ); - expect(model.crashlyticsConfig.enabled, isFalse); - expect(model.faroConfig.enabled, isTrue); - expect(model.faroConfig.endpoint, equals('https://faro.example.com')); + expect(model.crashlyticsConfig?.enabled, isFalse); + expect(model.faroConfig?.enabled, isTrue); + expect(model.faroConfig?.endpoint, equals('https://faro.example.com')); }); test('should initialize with both services enabled (mocked)', () async { final model = EngineBugTrackingModel( - crashlyticsConfig: const EngineCrashlyticsConfig(enabled: true), - faroConfig: const EngineFaroConfig( + crashlyticsConfig: EngineCrashlyticsConfig(enabled: true), + faroConfig: EngineFaroConfig( enabled: true, endpoint: 'https://faro.example.com', appName: 'TestApp', @@ -118,17 +118,17 @@ void main() { googleLoggingConfig: TestConfigs.googleLoggingConfig, ); - expect(model.crashlyticsConfig.enabled, isTrue); - expect(model.faroConfig.enabled, isTrue); - expect(model.faroConfig.appName, equals('TestApp')); + expect(model.crashlyticsConfig?.enabled, isTrue); + expect(model.faroConfig?.enabled, isTrue); + expect(model.faroConfig?.appName, equals('TestApp')); }); }); group('Method Calls', () { test('should handle custom key when services disabled', () async { final model = EngineBugTrackingModel( - crashlyticsConfig: const EngineCrashlyticsConfig(enabled: false), - faroConfig: const EngineFaroConfig( + crashlyticsConfig: EngineCrashlyticsConfig(enabled: false), + faroConfig: EngineFaroConfig( enabled: false, endpoint: '', appName: '', @@ -148,8 +148,8 @@ void main() { test('should handle user identifier when services disabled', () async { final model = EngineBugTrackingModel( - crashlyticsConfig: const EngineCrashlyticsConfig(enabled: false), - faroConfig: const EngineFaroConfig( + crashlyticsConfig: EngineCrashlyticsConfig(enabled: false), + faroConfig: EngineFaroConfig( enabled: false, endpoint: '', appName: '', @@ -170,8 +170,8 @@ void main() { test('should handle logging when services disabled', () async { final model = EngineBugTrackingModel( - crashlyticsConfig: const EngineCrashlyticsConfig(enabled: false), - faroConfig: const EngineFaroConfig( + crashlyticsConfig: EngineCrashlyticsConfig(enabled: false), + faroConfig: EngineFaroConfig( enabled: false, endpoint: '', appName: '', @@ -197,8 +197,8 @@ void main() { test('should handle error recording when services disabled', () async { final model = EngineBugTrackingModel( - crashlyticsConfig: const EngineCrashlyticsConfig(enabled: false), - faroConfig: const EngineFaroConfig( + crashlyticsConfig: EngineCrashlyticsConfig(enabled: false), + faroConfig: EngineFaroConfig( enabled: false, endpoint: '', appName: '', @@ -230,8 +230,8 @@ void main() { test('should handle Flutter error recording when services disabled', () async { final model = EngineBugTrackingModel( - crashlyticsConfig: const EngineCrashlyticsConfig(enabled: false), - faroConfig: const EngineFaroConfig( + crashlyticsConfig: EngineCrashlyticsConfig(enabled: false), + faroConfig: EngineFaroConfig( enabled: false, endpoint: '', appName: '', @@ -259,8 +259,8 @@ void main() { group('Configuration Checks', () { test('should correctly identify enabled services (configuration only)', () async { final crashlyticsModel = EngineBugTrackingModel( - crashlyticsConfig: const EngineCrashlyticsConfig(enabled: true), - faroConfig: const EngineFaroConfig( + crashlyticsConfig: EngineCrashlyticsConfig(enabled: true), + faroConfig: EngineFaroConfig( enabled: false, endpoint: '', appName: '', @@ -273,12 +273,12 @@ void main() { googleLoggingConfig: TestConfigs.googleLoggingConfig, ); - expect(crashlyticsModel.crashlyticsConfig.enabled, isTrue); - expect(crashlyticsModel.faroConfig.enabled, isFalse); + expect(crashlyticsModel.crashlyticsConfig?.enabled, isTrue); + expect(crashlyticsModel.faroConfig?.enabled, isFalse); final faroModel = EngineBugTrackingModel( - crashlyticsConfig: const EngineCrashlyticsConfig(enabled: false), - faroConfig: const EngineFaroConfig( + crashlyticsConfig: EngineCrashlyticsConfig(enabled: false), + faroConfig: EngineFaroConfig( enabled: true, endpoint: 'https://faro.example.com', appName: 'TestApp', @@ -291,8 +291,8 @@ void main() { googleLoggingConfig: TestConfigs.googleLoggingConfig, ); - expect(faroModel.crashlyticsConfig.enabled, isFalse); - expect(faroModel.faroConfig.enabled, isTrue); + expect(faroModel.crashlyticsConfig?.enabled, isFalse); + expect(faroModel.faroConfig?.enabled, isTrue); }); }); }); diff --git a/test/config/engine_crashlytics_config_test.dart b/test/config/engine_crashlytics_config_test.dart index 1f497c5..44a098a 100644 --- a/test/config/engine_crashlytics_config_test.dart +++ b/test/config/engine_crashlytics_config_test.dart @@ -4,44 +4,25 @@ import 'package:flutter_test/flutter_test.dart'; void main() { group('EngineCrashlyticsConfig', () { test('should create config with enabled true', () { - const config = EngineCrashlyticsConfig(enabled: true); + final config = EngineCrashlyticsConfig(enabled: true); expect(config.enabled, isTrue); }); test('should create config with enabled false', () { - const config = EngineCrashlyticsConfig(enabled: false); + final config = EngineCrashlyticsConfig(enabled: false); expect(config.enabled, isFalse); }); test('should have correct toString representation', () { - const config = EngineCrashlyticsConfig(enabled: true); + final config = EngineCrashlyticsConfig(enabled: true); expect(config.toString(), equals('EngineCrashlyticsConfig(enabled: true)')); }); - test('should have correct equality comparison', () { - const config1 = EngineCrashlyticsConfig(enabled: true); - const config2 = EngineCrashlyticsConfig(enabled: true); - const config3 = EngineCrashlyticsConfig(enabled: false); - - expect(config1, equals(config2)); - expect(config1, isNot(equals(config3))); - expect(config1 == config1, isTrue); - }); - - test('should have correct hashCode', () { - const config1 = EngineCrashlyticsConfig(enabled: true); - const config2 = EngineCrashlyticsConfig(enabled: true); - const config3 = EngineCrashlyticsConfig(enabled: false); - - expect(config1.hashCode, equals(config2.hashCode)); - expect(config1.hashCode, isNot(equals(config3.hashCode))); - }); - test('should not be equal to different types', () { - const config = EngineCrashlyticsConfig(enabled: true); + final config = EngineCrashlyticsConfig(enabled: true); expect(config, isNot(equals('string'))); expect(config, isNot(equals(42))); diff --git a/test/config/engine_faro_config_test.dart b/test/config/engine_faro_config_test.dart index 097efa6..01157aa 100644 --- a/test/config/engine_faro_config_test.dart +++ b/test/config/engine_faro_config_test.dart @@ -4,7 +4,7 @@ import 'package:flutter_test/flutter_test.dart'; void main() { group('EngineFaroConfig', () { test('should create config with all parameters', () { - const config = EngineFaroConfig( + final config = EngineFaroConfig( enabled: true, endpoint: 'https://faro.example.com', appName: 'TestApp', @@ -24,7 +24,7 @@ void main() { }); test('should create disabled config', () { - const config = EngineFaroConfig( + final config = EngineFaroConfig( enabled: false, endpoint: '', appName: '', @@ -44,7 +44,7 @@ void main() { }); test('should have correct toString representation with masked API key', () { - const config = EngineFaroConfig( + final config = EngineFaroConfig( enabled: true, endpoint: 'https://faro.example.com', appName: 'TestApp', @@ -66,7 +66,7 @@ void main() { }); test('should handle empty API key in toString', () { - const config = EngineFaroConfig( + final config = EngineFaroConfig( enabled: false, endpoint: '', appName: '', diff --git a/test/config/engine_firebase_analytics_config_test.dart b/test/config/engine_firebase_analytics_config_test.dart index 08820e2..06246f3 100644 --- a/test/config/engine_firebase_analytics_config_test.dart +++ b/test/config/engine_firebase_analytics_config_test.dart @@ -4,48 +4,21 @@ import 'package:flutter_test/flutter_test.dart'; void main() { group('EngineFirebaseAnalyticsConfig', () { test('should create config with enabled true', () { - const config = EngineFirebaseAnalyticsConfig(enabled: true); + final config = EngineFirebaseAnalyticsConfig(enabled: true); expect(config.enabled, isTrue); }); test('should create config with enabled false', () { - const config = EngineFirebaseAnalyticsConfig(enabled: false); + final config = EngineFirebaseAnalyticsConfig(enabled: false); expect(config.enabled, isFalse); }); test('should have correct toString representation', () { - const config = EngineFirebaseAnalyticsConfig(enabled: true); + final config = EngineFirebaseAnalyticsConfig(enabled: true); expect(config.toString(), equals('EngineFirebaseAnalyticsConfig(enabled: true)')); }); - - test('should have correct equality comparison', () { - const config1 = EngineFirebaseAnalyticsConfig(enabled: true); - const config2 = EngineFirebaseAnalyticsConfig(enabled: true); - const config3 = EngineFirebaseAnalyticsConfig(enabled: false); - - expect(config1, equals(config2)); - expect(config1, isNot(equals(config3))); - expect(config1 == config1, isTrue); - }); - - test('should have correct hashCode', () { - const config1 = EngineFirebaseAnalyticsConfig(enabled: true); - const config2 = EngineFirebaseAnalyticsConfig(enabled: true); - const config3 = EngineFirebaseAnalyticsConfig(enabled: false); - - expect(config1.hashCode, equals(config2.hashCode)); - expect(config1.hashCode, isNot(equals(config3.hashCode))); - }); - - test('should not be equal to different types', () { - const config = EngineFirebaseAnalyticsConfig(enabled: true); - - expect(config, isNot(equals('string'))); - expect(config, isNot(equals(42))); - expect(config, isNot(equals(null))); - }); }); } diff --git a/test/config/engine_splunk_config_test.dart b/test/config/engine_splunk_config_test.dart index 79bb5a7..c41aaf0 100644 --- a/test/config/engine_splunk_config_test.dart +++ b/test/config/engine_splunk_config_test.dart @@ -4,7 +4,7 @@ import 'package:flutter_test/flutter_test.dart'; void main() { group('EngineSplunkConfig', () { test('should create instance with all parameters', () { - const config = EngineSplunkConfig( + final config = EngineSplunkConfig( enabled: true, endpoint: 'https://splunk-hec.example.com:8088/services/collector', token: 'test-hec-token', @@ -22,7 +22,7 @@ void main() { }); test('should create disabled instance', () { - const config = EngineSplunkConfig( + final config = EngineSplunkConfig( enabled: false, endpoint: '', token: '', @@ -40,7 +40,7 @@ void main() { }); test('toString should mask token', () { - const config = EngineSplunkConfig( + final config = EngineSplunkConfig( enabled: true, endpoint: 'https://splunk.example.com', token: 'secret-token-123', @@ -59,41 +59,8 @@ void main() { expect(stringRepresentation, isNot(contains('secret-token-123'))); }); - test('should implement equality correctly', () { - const config1 = EngineSplunkConfig( - enabled: true, - endpoint: 'https://example.com', - token: 'token', - source: 'source', - sourcetype: 'json', - index: 'main', - ); - - const config2 = EngineSplunkConfig( - enabled: true, - endpoint: 'https://example.com', - token: 'token', - source: 'source', - sourcetype: 'json', - index: 'main', - ); - - const config3 = EngineSplunkConfig( - enabled: false, - endpoint: 'https://example.com', - token: 'token', - source: 'source', - sourcetype: 'json', - index: 'main', - ); - - expect(config1, equals(config2)); - expect(config1, isNot(equals(config3))); - expect(config1.hashCode, equals(config2.hashCode)); - }); - test('should not be equal to different types', () { - const config = EngineSplunkConfig( + final config = EngineSplunkConfig( enabled: true, endpoint: 'https://example.com', token: 'token', diff --git a/test/engine_tracking_initialize_test.dart b/test/engine_tracking_initialize_test.dart index 146e83f..733a043 100644 --- a/test/engine_tracking_initialize_test.dart +++ b/test/engine_tracking_initialize_test.dart @@ -10,9 +10,9 @@ void main() { group('initWithAdapters', () { test('should initialize both analytics and bug tracking with adapters', () async { final analyticsAdapters = [ - EngineFirebaseAnalyticsAdapter(const EngineFirebaseAnalyticsConfig(enabled: false)), + EngineFirebaseAnalyticsAdapter(EngineFirebaseAnalyticsConfig(enabled: false)), EngineFaroAnalyticsAdapter( - const EngineFaroConfig( + EngineFaroConfig( enabled: false, endpoint: '', appName: '', @@ -26,9 +26,9 @@ void main() { ]; final bugTrackingAdapters = [ - EngineCrashlyticsAdapter(const EngineCrashlyticsConfig(enabled: false)), + EngineCrashlyticsAdapter(EngineCrashlyticsConfig(enabled: false)), EngineFaroBugTrackingAdapter( - const EngineFaroConfig( + EngineFaroConfig( enabled: false, endpoint: '', appName: '', @@ -53,7 +53,7 @@ void main() { test('should initialize only analytics when bug tracking is null', () async { final analyticsAdapters = [ - EngineFirebaseAnalyticsAdapter(const EngineFirebaseAnalyticsConfig(enabled: false)), + EngineFirebaseAnalyticsAdapter(EngineFirebaseAnalyticsConfig(enabled: false)), ]; await EngineTrackingInitialize.initWithAdapters( @@ -68,7 +68,7 @@ void main() { test('should initialize only bug tracking when analytics is null', () async { final bugTrackingAdapters = [ - EngineCrashlyticsAdapter(const EngineCrashlyticsConfig(enabled: false)), + EngineCrashlyticsAdapter(EngineCrashlyticsConfig(enabled: false)), ]; await EngineTrackingInitialize.initWithAdapters( @@ -108,9 +108,9 @@ void main() { group('initWithModels', () { test('should initialize both analytics and bug tracking with models', () async { final analyticsModel = EngineAnalyticsModel( - clarityConfig: const EngineClarityConfig(enabled: false, projectId: ''), - firebaseAnalyticsConfig: const EngineFirebaseAnalyticsConfig(enabled: false), - faroConfig: const EngineFaroConfig( + clarityConfig: EngineClarityConfig(enabled: false, projectId: ''), + firebaseAnalyticsConfig: EngineFirebaseAnalyticsConfig(enabled: false), + faroConfig: EngineFaroConfig( enabled: false, endpoint: '', appName: '', @@ -120,13 +120,13 @@ void main() { namespace: '', platform: '', ), - googleLoggingConfig: const EngineGoogleLoggingConfig( + googleLoggingConfig: EngineGoogleLoggingConfig( enabled: false, projectId: '', logName: '', credentials: {}, ), - splunkConfig: const EngineSplunkConfig( + splunkConfig: EngineSplunkConfig( enabled: false, endpoint: '', token: '', @@ -137,8 +137,8 @@ void main() { ); final bugTrackingModel = EngineBugTrackingModel( - crashlyticsConfig: const EngineCrashlyticsConfig(enabled: false), - faroConfig: const EngineFaroConfig( + crashlyticsConfig: EngineCrashlyticsConfig(enabled: false), + faroConfig: EngineFaroConfig( enabled: false, endpoint: '', appName: '', @@ -148,7 +148,7 @@ void main() { namespace: '', platform: '', ), - googleLoggingConfig: const EngineGoogleLoggingConfig( + googleLoggingConfig: EngineGoogleLoggingConfig( enabled: false, projectId: '', logName: '', @@ -168,9 +168,9 @@ void main() { test('should initialize only analytics when bug tracking is null', () async { final analyticsModel = EngineAnalyticsModel( - clarityConfig: const EngineClarityConfig(enabled: false, projectId: ''), - firebaseAnalyticsConfig: const EngineFirebaseAnalyticsConfig(enabled: false), - faroConfig: const EngineFaroConfig( + clarityConfig: EngineClarityConfig(enabled: false, projectId: ''), + firebaseAnalyticsConfig: EngineFirebaseAnalyticsConfig(enabled: false), + faroConfig: EngineFaroConfig( enabled: false, endpoint: '', appName: '', @@ -180,13 +180,13 @@ void main() { namespace: '', platform: '', ), - googleLoggingConfig: const EngineGoogleLoggingConfig( + googleLoggingConfig: EngineGoogleLoggingConfig( enabled: false, projectId: '', logName: '', credentials: {}, ), - splunkConfig: const EngineSplunkConfig( + splunkConfig: EngineSplunkConfig( enabled: false, endpoint: '', token: '', @@ -208,8 +208,8 @@ void main() { test('should initialize only bug tracking when analytics is null', () async { final bugTrackingModel = EngineBugTrackingModel( - crashlyticsConfig: const EngineCrashlyticsConfig(enabled: false), - faroConfig: const EngineFaroConfig( + crashlyticsConfig: EngineCrashlyticsConfig(enabled: false), + faroConfig: EngineFaroConfig( enabled: false, endpoint: '', appName: '', @@ -219,7 +219,7 @@ void main() { namespace: '', platform: '', ), - googleLoggingConfig: const EngineGoogleLoggingConfig( + googleLoggingConfig: EngineGoogleLoggingConfig( enabled: false, projectId: '', logName: '', @@ -294,9 +294,9 @@ void main() { group('isEnabled', () { test('should return true when at least one service is enabled', () async { final analyticsModel = EngineAnalyticsModel( - clarityConfig: const EngineClarityConfig(enabled: false, projectId: ''), - firebaseAnalyticsConfig: const EngineFirebaseAnalyticsConfig(enabled: true), - faroConfig: const EngineFaroConfig( + clarityConfig: EngineClarityConfig(enabled: false, projectId: ''), + firebaseAnalyticsConfig: EngineFirebaseAnalyticsConfig(enabled: true), + faroConfig: EngineFaroConfig( enabled: false, endpoint: '', appName: '', @@ -306,13 +306,13 @@ void main() { namespace: '', platform: '', ), - googleLoggingConfig: const EngineGoogleLoggingConfig( + googleLoggingConfig: EngineGoogleLoggingConfig( enabled: false, projectId: '', logName: '', credentials: {}, ), - splunkConfig: const EngineSplunkConfig( + splunkConfig: EngineSplunkConfig( enabled: false, endpoint: '', token: '', diff --git a/test/helpers/test_configs.dart b/test/helpers/test_configs.dart index 988dc9f..dc3303d 100644 --- a/test/helpers/test_configs.dart +++ b/test/helpers/test_configs.dart @@ -5,7 +5,7 @@ class TestConfigs { TestConfigs._(); /// Configuração padrĆ£o do Google Cloud Logging para testes - static const googleLoggingConfig = EngineGoogleLoggingConfig( + static final googleLoggingConfig = EngineGoogleLoggingConfig( enabled: false, projectId: '', logName: '', @@ -13,7 +13,7 @@ class TestConfigs { ); /// Configuração habilitada do Google Cloud Logging para testes - static const googleLoggingConfigEnabled = EngineGoogleLoggingConfig( + static final googleLoggingConfigEnabled = EngineGoogleLoggingConfig( enabled: true, projectId: 'test-project', logName: 'test-logs', diff --git a/test/models/engine_analytics_model_test.dart b/test/models/engine_analytics_model_test.dart index 80dcdaa..6a66424 100644 --- a/test/models/engine_analytics_model_test.dart +++ b/test/models/engine_analytics_model_test.dart @@ -6,8 +6,8 @@ import '../helpers/test_configs.dart'; void main() { group('EngineAnalyticsModel', () { test('should create instance with valid configs', () { - const firebaseConfig = EngineFirebaseAnalyticsConfig(enabled: true); - const faroConfig = EngineFaroConfig( + final firebaseConfig = EngineFirebaseAnalyticsConfig(enabled: true); + final faroConfig = EngineFaroConfig( enabled: true, endpoint: 'https://example.com', appName: 'TestApp', @@ -17,7 +17,7 @@ void main() { namespace: '', platform: '', ); - const splunkConfig = EngineSplunkConfig( + final splunkConfig = EngineSplunkConfig( enabled: true, endpoint: 'https://splunk.com', token: 'test-token', @@ -25,7 +25,7 @@ void main() { sourcetype: 'json', index: 'main', ); - const clarityConfig = EngineClarityConfig( + final clarityConfig = EngineClarityConfig( enabled: false, projectId: '', ); @@ -46,8 +46,8 @@ void main() { }); test('should not be equal to different types', () { - const firebaseConfig = EngineFirebaseAnalyticsConfig(enabled: false); - const faroConfig = EngineFaroConfig( + final firebaseConfig = EngineFirebaseAnalyticsConfig(enabled: false); + final faroConfig = EngineFaroConfig( enabled: false, endpoint: '', appName: '', @@ -57,7 +57,7 @@ void main() { namespace: '', platform: '', ); - const splunkConfig = EngineSplunkConfig( + final splunkConfig = EngineSplunkConfig( enabled: false, endpoint: '', token: '', @@ -71,7 +71,7 @@ void main() { faroConfig: faroConfig, googleLoggingConfig: TestConfigs.googleLoggingConfig, splunkConfig: splunkConfig, - clarityConfig: const EngineClarityConfig( + clarityConfig: EngineClarityConfig( enabled: false, projectId: '', ), @@ -83,8 +83,8 @@ void main() { }); test('toString should include all configurations', () { - const firebaseConfig = EngineFirebaseAnalyticsConfig(enabled: true); - const faroConfig = EngineFaroConfig( + final firebaseConfig = EngineFirebaseAnalyticsConfig(enabled: true); + final faroConfig = EngineFaroConfig( enabled: true, endpoint: 'https://example.com', appName: 'TestApp', @@ -94,7 +94,7 @@ void main() { namespace: '', platform: '', ); - const splunkConfig = EngineSplunkConfig( + final splunkConfig = EngineSplunkConfig( enabled: true, endpoint: 'https://splunk.com', token: 'test-token', @@ -108,7 +108,7 @@ void main() { faroConfig: faroConfig, googleLoggingConfig: TestConfigs.googleLoggingConfigEnabled, splunkConfig: splunkConfig, - clarityConfig: const EngineClarityConfig( + clarityConfig: EngineClarityConfig( enabled: false, projectId: '', ), diff --git a/test/models/engine_bug_tracking_model_test.dart b/test/models/engine_bug_tracking_model_test.dart index a5c2467..cc091d0 100644 --- a/test/models/engine_bug_tracking_model_test.dart +++ b/test/models/engine_bug_tracking_model_test.dart @@ -8,8 +8,8 @@ import '../helpers/test_configs.dart'; void main() { group('EngineBugTrackingModel', () { test('should create model with configurations', () { - const crashlyticsConfig = EngineCrashlyticsConfig(enabled: true); - const faroConfig = EngineFaroConfig( + final crashlyticsConfig = EngineCrashlyticsConfig(enabled: true); + final faroConfig = EngineFaroConfig( enabled: true, endpoint: 'https://faro.example.com', appName: 'TestApp', @@ -32,8 +32,8 @@ void main() { }); test('should have correct toString representation', () { - const crashlyticsConfig = EngineCrashlyticsConfig(enabled: true); - const faroConfig = EngineFaroConfig( + final crashlyticsConfig = EngineCrashlyticsConfig(enabled: true); + final faroConfig = EngineFaroConfig( enabled: false, endpoint: '', appName: '', diff --git a/test/test_coverage.dart b/test/test_coverage.dart index 6eeaa04..3d31c11 100644 --- a/test/test_coverage.dart +++ b/test/test_coverage.dart @@ -6,9 +6,9 @@ import 'helpers/test_configs.dart'; void main() { group('Test Coverage', () { test('should import all classes correctly', () { - expect(() => const EngineFirebaseAnalyticsConfig(enabled: true), returnsNormally); + expect(() => EngineFirebaseAnalyticsConfig(enabled: true), returnsNormally); expect( - () => const EngineFaroConfig( + () => EngineFaroConfig( enabled: false, endpoint: '', appName: '', @@ -21,7 +21,7 @@ void main() { returnsNormally, ); expect( - () => const EngineSplunkConfig( + () => EngineSplunkConfig( enabled: false, endpoint: '', token: '', @@ -32,7 +32,7 @@ void main() { returnsNormally, ); expect( - () => const EngineGoogleLoggingConfig( + () => EngineGoogleLoggingConfig( enabled: false, projectId: '', logName: '', @@ -42,8 +42,8 @@ void main() { ); expect( () => EngineAnalyticsModel( - firebaseAnalyticsConfig: const EngineFirebaseAnalyticsConfig(enabled: false), - faroConfig: const EngineFaroConfig( + firebaseAnalyticsConfig: EngineFirebaseAnalyticsConfig(enabled: false), + faroConfig: EngineFaroConfig( enabled: false, endpoint: '', appName: '', @@ -54,7 +54,7 @@ void main() { platform: '', ), googleLoggingConfig: TestConfigs.googleLoggingConfig, - splunkConfig: const EngineSplunkConfig( + splunkConfig: EngineSplunkConfig( enabled: false, endpoint: '', token: '', @@ -62,7 +62,7 @@ void main() { sourcetype: '', index: '', ), - clarityConfig: const EngineClarityConfig( + clarityConfig: EngineClarityConfig( enabled: false, projectId: '', ), @@ -71,11 +71,11 @@ void main() { ); expect(EngineAnalyticsModelDefault.new, returnsNormally); - expect(() => const EngineCrashlyticsConfig(enabled: true), returnsNormally); + expect(() => EngineCrashlyticsConfig(enabled: true), returnsNormally); expect( () => EngineBugTrackingModel( - crashlyticsConfig: const EngineCrashlyticsConfig(enabled: false), - faroConfig: const EngineFaroConfig( + crashlyticsConfig: EngineCrashlyticsConfig(enabled: false), + faroConfig: EngineFaroConfig( enabled: false, endpoint: '', appName: '', @@ -103,13 +103,13 @@ void main() { }); test('should handle edge cases', () { - const firebaseConfig = EngineFirebaseAnalyticsConfig(enabled: true); + final firebaseConfig = EngineFirebaseAnalyticsConfig(enabled: true); expect(firebaseConfig.toString(), isNotEmpty); - const crashlyticsConfig = EngineCrashlyticsConfig(enabled: false); + final crashlyticsConfig = EngineCrashlyticsConfig(enabled: false); expect(crashlyticsConfig.toString(), isNotEmpty); - const faroConfig = EngineFaroConfig( + final faroConfig = EngineFaroConfig( enabled: true, endpoint: 'test', appName: 'test', @@ -122,7 +122,7 @@ void main() { expect(faroConfig.toString(), isNotEmpty); expect(faroConfig.toString(), contains('****')); - const splunkConfig = EngineSplunkConfig( + final splunkConfig = EngineSplunkConfig( enabled: true, endpoint: 'https://splunk.com', token: 'secret-token', @@ -133,7 +133,7 @@ void main() { expect(splunkConfig.toString(), isNotEmpty); expect(splunkConfig.toString(), contains('****')); - const googleLoggingConfig = EngineGoogleLoggingConfig( + final googleLoggingConfig = EngineGoogleLoggingConfig( enabled: true, projectId: 'test-project', logName: 'test-logs', diff --git a/test/utils/engine_http_override_test.dart b/test/utils/engine_http_override_test.dart new file mode 100644 index 0000000..89ae5f3 --- /dev/null +++ b/test/utils/engine_http_override_test.dart @@ -0,0 +1,98 @@ +import 'dart:io'; + +import 'package:engine_tracking/src/http/engine_http_override.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + group('EngineHttpOverride', () { + tearDown(() { + HttpOverrides.global = null; + }); + + test('should create HTTP client with logging capabilities', () { + final override = EngineHttpOverride( + enableRequestLogging: true, + enableResponseLogging: true, + logName: 'TEST_HTTP', + ); + + final client = override.createHttpClient(null); + expect(client, isNotNull); + expect(client, isA()); + }); + + test('should chain with base override', () { + final baseOverride = _MockHttpOverride(); + final override = EngineHttpOverride( + existingOverrides: baseOverride, + logName: 'TEST_HTTP', + ); + + final client = override.createHttpClient(null); + expect(client, isNotNull); + }); + + test('should configure logging options correctly', () { + final override = EngineHttpOverride( + enableRequestLogging: false, + enableResponseLogging: true, + enableTimingLogging: false, + enableHeaderLogging: true, + enableBodyLogging: false, + maxBodyLogLength: 500, + logName: 'CUSTOM_LOG', + ); + + expect(override.enableRequestLogging, isFalse); + expect(override.enableResponseLogging, isTrue); + expect(override.enableTimingLogging, isFalse); + expect(override.enableHeaderLogging, isTrue); + expect(override.enableBodyLogging, isFalse); + expect(override.maxBodyLogLength, equals(500)); + expect(override.logName, equals('CUSTOM_LOG')); + }); + + test('should use default configuration values', () { + final override = EngineHttpOverride(); + + expect(override.enableRequestLogging, isTrue); + expect(override.enableResponseLogging, isTrue); + expect(override.enableTimingLogging, isTrue); + expect(override.enableHeaderLogging, isFalse); + expect(override.enableBodyLogging, isFalse); + expect(override.maxBodyLogLength, equals(1000)); + expect(override.logName, equals('HTTP_TRACKING')); + }); + + group('Integration with HttpOverrides.global', () { + test('should set global override correctly', () { + final override = EngineHttpOverride(logName: 'GLOBAL_TEST'); + HttpOverrides.global = override; + + expect(HttpOverrides.current, equals(override)); + }); + + test('should restore previous override when disabled', () { + final previousOverride = _MockHttpOverride(); + HttpOverrides.global = previousOverride; + + final engineOverride = EngineHttpOverride( + existingOverrides: previousOverride, + logName: 'ENGINE_TEST', + ); + HttpOverrides.global = engineOverride; + + expect(HttpOverrides.current, equals(engineOverride)); + + // Simulate restoration + HttpOverrides.global = previousOverride; + expect(HttpOverrides.current, equals(previousOverride)); + }); + }); + }); +} + +class _MockHttpOverride extends HttpOverrides { + @override + HttpClient createHttpClient(final SecurityContext? context) => HttpClient(context: context); +} diff --git a/test/utils/engine_http_tracking_test.dart b/test/utils/engine_http_tracking_test.dart new file mode 100644 index 0000000..0db560f --- /dev/null +++ b/test/utils/engine_http_tracking_test.dart @@ -0,0 +1,186 @@ +import 'dart:io'; + +import 'package:engine_tracking/engine_tracking.dart'; +import 'package:engine_tracking/src/http/engine_http_override.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + group('EngineHttpTracking', () { + tearDown(() { + EngineHttpTracking.disable(); + HttpOverrides.global = null; + }); + + test('should initialize with valid configuration', () { + final config = EngineHttpTrackingConfig( + enabled: true, + enableRequestLogging: true, + enableResponseLogging: true, + logName: 'TEST_HTTP', + ); + + EngineHttpTracking.initialize(config); + + expect(EngineHttpTracking.isEnabled, isTrue); + expect(EngineHttpTracking.config, equals(config)); + expect(HttpOverrides.current, isA()); + }); + + test('should not initialize when disabled', () { + final config = EngineHttpTrackingConfig( + enabled: false, + logName: 'TEST_HTTP', + ); + + EngineHttpTracking.initialize(config); + + expect(EngineHttpTracking.isEnabled, isFalse); + expect(EngineHttpTracking.config, isNull); + }); + + test('should update configuration', () { + final initialConfig = EngineHttpTrackingConfig( + enabled: true, + enableRequestLogging: true, + logName: 'INITIAL', + ); + + final updatedConfig = EngineHttpTrackingConfig( + enabled: true, + enableRequestLogging: false, + enableResponseLogging: true, + logName: 'UPDATED', + ); + + EngineHttpTracking.initialize(initialConfig); + expect(EngineHttpTracking.config?.logName, equals('INITIAL')); + + EngineHttpTracking.updateConfig(updatedConfig); + expect(EngineHttpTracking.config?.logName, equals('UPDATED')); + expect(EngineHttpTracking.config?.enableRequestLogging, isFalse); + expect(EngineHttpTracking.config?.enableResponseLogging, isTrue); + }); + + test('should provide temporary disable functionality', () { + final config = EngineHttpTrackingConfig( + enabled: true, + logName: 'TEST_HTTP', + ); + + EngineHttpTracking.initialize(config); + expect(EngineHttpTracking.isEnabled, isTrue); + expect(EngineHttpTracking.config?.logName, equals('TEST_HTTP')); + }); + + test('should execute with scoped configuration', () async { + final initialConfig = EngineHttpTrackingConfig( + enabled: true, + logName: 'INITIAL', + ); + + final scopedConfig = EngineHttpTrackingConfig( + enabled: true, + logName: 'SCOPED', + ); + + EngineHttpTracking.initialize(initialConfig); + expect(EngineHttpTracking.config?.logName, equals('INITIAL')); + + await EngineHttpTracking.withConfig(scopedConfig, () async { + expect(EngineHttpTracking.config?.logName, equals('SCOPED')); + await Future.delayed(const Duration(milliseconds: 10)); + }); + + expect(EngineHttpTracking.config?.logName, equals('INITIAL')); + }); + + test('should return correct stats', () { + // First disable any existing tracking + EngineHttpTracking.disable(); + + final config = EngineHttpTrackingConfig( + enabled: true, + logName: 'TEST_HTTP', + ); + + final stats = EngineHttpTracking.getStats(); + expect(stats['is_enabled'], isFalse); + expect(stats['has_config'], isFalse); + + EngineHttpTracking.initialize(config); + + final enabledStats = EngineHttpTracking.getStats(); + expect(enabledStats['is_enabled'], isTrue); + expect(enabledStats['has_config'], isTrue); + expect(enabledStats['current_override'], contains('EngineHttpOverride')); + }); + + group('Configuration validation', () { + test('should create development-like configuration', () { + final config = EngineHttpTrackingConfig( + enabled: true, + enableRequestLogging: true, + enableResponseLogging: true, + enableTimingLogging: true, + enableHeaderLogging: true, + enableBodyLogging: true, + maxBodyLogLength: 2000, + logName: 'HTTP_TRACKING_DEV', + ); + + expect(config.enabled, isTrue); + expect(config.enableRequestLogging, isTrue); + expect(config.enableResponseLogging, isTrue); + expect(config.enableTimingLogging, isTrue); + expect(config.enableHeaderLogging, isTrue); + expect(config.enableBodyLogging, isTrue); + expect(config.maxBodyLogLength, equals(2000)); + expect(config.logName, equals('HTTP_TRACKING_DEV')); + }); + + test('should create production-like configuration', () { + final config = EngineHttpTrackingConfig( + enabled: true, + enableRequestLogging: true, + enableResponseLogging: true, + enableTimingLogging: true, + enableHeaderLogging: false, + enableBodyLogging: false, + maxBodyLogLength: 500, + logName: 'HTTP_TRACKING_PROD', + ); + + expect(config.enabled, isTrue); + expect(config.enableRequestLogging, isTrue); + expect(config.enableResponseLogging, isTrue); + expect(config.enableTimingLogging, isTrue); + expect(config.enableHeaderLogging, isFalse); + expect(config.enableBodyLogging, isFalse); + expect(config.maxBodyLogLength, equals(500)); + expect(config.logName, equals('HTTP_TRACKING_PROD')); + }); + + test('should create errors-only configuration', () { + final config = EngineHttpTrackingConfig( + enabled: true, + enableRequestLogging: false, + enableResponseLogging: true, + enableTimingLogging: true, + enableHeaderLogging: false, + enableBodyLogging: false, + maxBodyLogLength: 0, + logName: 'HTTP_ERRORS', + ); + + expect(config.enabled, isTrue); + expect(config.enableRequestLogging, isFalse); + expect(config.enableResponseLogging, isTrue); + expect(config.enableTimingLogging, isTrue); + expect(config.enableHeaderLogging, isFalse); + expect(config.enableBodyLogging, isFalse); + expect(config.maxBodyLogLength, equals(0)); + expect(config.logName, equals('HTTP_ERRORS')); + }); + }); + }); +} diff --git a/test/widgets/engine_widget_test.dart b/test/widgets/engine_widget_test.dart new file mode 100644 index 0000000..3da2a35 --- /dev/null +++ b/test/widgets/engine_widget_test.dart @@ -0,0 +1,191 @@ +import 'package:engine_tracking/engine_tracking.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + group('EngineWidget', () { + tearDown(() async { + await EngineAnalytics.dispose(); + }); + + testWidgets('should return app unchanged when Clarity is not initialized', (final tester) async { + const testApp = MaterialApp(home: Text('Test App')); + + const widget = EngineWidget(app: testApp); + + await tester.pumpWidget(widget); + + expect(find.text('Test App'), findsOneWidget); + }); + + testWidgets('should return app unchanged when Clarity is disabled', (final tester) async { + // Initialize analytics with disabled Clarity + final analyticsModel = EngineAnalyticsModel( + clarityConfig: EngineClarityConfig( + enabled: false, + projectId: 'test-project', + ), + firebaseAnalyticsConfig: EngineFirebaseAnalyticsConfig(enabled: false), + faroConfig: EngineFaroConfig( + enabled: false, + endpoint: '', + appName: '', + appVersion: '', + environment: '', + apiKey: '', + namespace: '', + platform: '', + ), + splunkConfig: EngineSplunkConfig( + enabled: false, + endpoint: '', + token: '', + source: '', + sourcetype: '', + index: '', + ), + googleLoggingConfig: EngineGoogleLoggingConfig( + enabled: false, + projectId: '', + logName: '', + credentials: {}, + ), + ); + + await EngineAnalytics.initWithModel(analyticsModel); + + const testApp = MaterialApp(home: Text('Test App')); + const widget = EngineWidget(app: testApp); + + await tester.pumpWidget(widget); + + expect(find.text('Test App'), findsOneWidget); + }); + + testWidgets('should wrap app with ClarityWidget when Clarity is enabled', (final tester) async { + // Initialize analytics with enabled Clarity + final analyticsModel = EngineAnalyticsModel( + clarityConfig: EngineClarityConfig( + enabled: true, + projectId: 'test-project', + ), + firebaseAnalyticsConfig: EngineFirebaseAnalyticsConfig(enabled: false), + faroConfig: EngineFaroConfig( + enabled: false, + endpoint: '', + appName: '', + appVersion: '', + environment: '', + apiKey: '', + namespace: '', + platform: '', + ), + splunkConfig: EngineSplunkConfig( + enabled: false, + endpoint: '', + token: '', + source: '', + sourcetype: '', + index: '', + ), + googleLoggingConfig: EngineGoogleLoggingConfig( + enabled: false, + projectId: '', + logName: '', + credentials: {}, + ), + ); + + await EngineAnalytics.initWithModel(analyticsModel); + + const testApp = MaterialApp(home: Text('Test App')); + const widget = EngineWidget(app: testApp); + + await tester.pumpWidget(widget); + + // The app should still be findable, but wrapped in ClarityWidget + expect(find.text('Test App'), findsOneWidget); + }); + + test('should get Clarity configuration from EngineAnalytics', () async { + final clarityConfig = EngineClarityConfig( + enabled: true, + projectId: 'test-project-123', + ); + + final analyticsModel = EngineAnalyticsModel( + clarityConfig: clarityConfig, + firebaseAnalyticsConfig: EngineFirebaseAnalyticsConfig(enabled: false), + faroConfig: EngineFaroConfig( + enabled: false, + endpoint: '', + appName: '', + appVersion: '', + environment: '', + apiKey: '', + namespace: '', + platform: '', + ), + splunkConfig: EngineSplunkConfig( + enabled: false, + endpoint: '', + token: '', + source: '', + sourcetype: '', + index: '', + ), + googleLoggingConfig: EngineGoogleLoggingConfig( + enabled: false, + projectId: '', + logName: '', + credentials: {}, + ), + ); + + await EngineAnalytics.initWithModel(analyticsModel); + + final retrievedConfig = EngineAnalytics.getConfig(); + + expect(retrievedConfig, isNotNull); + expect(retrievedConfig!.enabled, isTrue); + expect(retrievedConfig.projectId, equals('test-project-123')); + }); + + test('should return null when Clarity is not configured', () async { + final analyticsModel = EngineAnalyticsModel( + clarityConfig: EngineClarityConfig(enabled: false, projectId: ''), + firebaseAnalyticsConfig: EngineFirebaseAnalyticsConfig(enabled: false), + faroConfig: EngineFaroConfig( + enabled: false, + endpoint: '', + appName: '', + appVersion: '', + environment: '', + apiKey: '', + namespace: '', + platform: '', + ), + splunkConfig: EngineSplunkConfig( + enabled: false, + endpoint: '', + token: '', + source: '', + sourcetype: '', + index: '', + ), + googleLoggingConfig: EngineGoogleLoggingConfig( + enabled: false, + projectId: '', + logName: '', + credentials: {}, + ), + ); + + await EngineAnalytics.initWithModel(analyticsModel); + + final retrievedConfig = EngineAnalytics.getConfig(); + + expect(retrievedConfig, isNull); + }); + }); +}