Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
133 commits
Select commit Hold shift + click to select a range
f192dac
feat(analytics): add environment variables for analytics providers
fulleni Dec 16, 2025
69aa290
build(core): update dependency ref
fulleni Dec 16, 2025
18403c9
feat(analytics): expose analytics credentials in EnvironmentConfig
fulleni Dec 16, 2025
d052b75
feat(analytics): add models for Google Analytics Data API responses
fulleni Dec 16, 2025
d8c9d96
feat(analytics): add models for Mixpanel API responses
fulleni Dec 16, 2025
968d7bc
chore: barrels
fulleni Dec 16, 2025
41be562
feat(analytics): define abstract analytics reporting client
fulleni Dec 16, 2025
51d464a
feat(analytics): implement Google Analytics data client
fulleni Dec 16, 2025
124dbfd
feat(analytics): implement Mixpanel data client
fulleni Dec 16, 2025
ba0bf50
chore: add intl dep
fulleni Dec 16, 2025
0235fe7
feat(analytics): implement analytics sync service
fulleni Dec 16, 2025
ec9624a
feat(analytics): create entry point for analytics sync worker
fulleni Dec 16, 2025
53ebe1e
feat(analytics): add analytics read permission
fulleni Dec 16, 2025
41d76f2
feat(analytics): grant analytics permission to admin role
fulleni Dec 16, 2025
8ac9b80
feat(analytics): register analytics operations and remove dashboard s…
fulleni Dec 16, 2025
5a73c57
fix: miss import
fulleni Dec 16, 2025
1c019d5
feat(analytics): register analytics operations and remove dashboard s…
fulleni Dec 16, 2025
2e33fc5
feat(analytics): seed analytics data collections
fulleni Dec 16, 2025
323d9ff
chore: enhance class docs
fulleni Dec 16, 2025
fa9fd9b
refactor(registry): remove dashboard_summary model config
fulleni Dec 17, 2025
ab10f82
feat(analytics): enhance analytics sync service with headline and met…
fulleni Dec 17, 2025
0321884
refactor(middleware): replace DashboardSummaryService with individual…
fulleni Dec 17, 2025
9fe08f8
feat(analytics): implement getRankedList method for Google Analytics
fulleni Dec 17, 2025
6bc592f
feat(analytics): implement ranked list functionality and enhance metr…
fulleni Dec 17, 2025
42edc96
feat(analytics): implement metric mapper for analytics providers
fulleni Dec 17, 2025
090a946
feat(analytics): implement synchronous data fetching and mapping for …
fulleni Dec 17, 2025
5084570
feat(analytics): add analytics metric mapper export
fulleni Dec 17, 2025
3908c30
refactor(dependencies): remove redundant instance call
fulleni Dec 17, 2025
89ad504
build(serialization): sync
fulleni Dec 17, 2025
7093c5e
feat(analytics): introduce structured AnalyticsQuery model
fulleni Dec 17, 2025
f5bd373
chore: barrels
fulleni Dec 17, 2025
dcfbe65
refactor(analytics): update client interface to use AnalyticsQuery
fulleni Dec 17, 2025
b99fb99
refactor(analytics): implement full logic in GoogleAnalyticsDataClient
fulleni Dec 17, 2025
af8477d
refactor(analytics): implement full logic in MixpanelDataClient
fulleni Dec 17, 2025
417bc53
feat(analytics): overhaul AnalyticsMetricMapper
fulleni Dec 17, 2025
a45d447
refactor(analytics): simplify AnalyticsSyncService to an orchestrator
fulleni Dec 17, 2025
c6d0d18
refactor(analytics): introduce specific metric query types
fulleni Dec 17, 2025
c0fb986
feat(analytics): add strongly-typed GA4 request models
fulleni Dec 17, 2025
7d31d78
chore: barrels
fulleni Dec 17, 2025
acb71aa
refactor(analytics): update client interface for type safety
fulleni Dec 17, 2025
b38de13
feat(analytics): fully implement all card ID mappings
fulleni Dec 17, 2025
2e2f286
refactor(analytics): use typed request models in GA4 client
fulleni Dec 17, 2025
512ff2a
refactor(analytics): align Mixpanel client with new patterns
fulleni Dec 17, 2025
2c34f59
feat(analytics): implement internal database metric queries
fulleni Dec 17, 2025
c284eef
feat(analytics): provide additional repositories to sync service
fulleni Dec 17, 2025
066f754
fix(analytics): add fromJson factories to GA4 request models
fulleni Dec 17, 2025
2b73f3b
feat(analytics): add strongly-typed Mixpanel request models
fulleni Dec 17, 2025
0609d5b
chore: barrels
fulleni Dec 17, 2025
d71c535
fix(analytics): correct type definitions in AnalyticsMetricMapper
fulleni Dec 17, 2025
422b0c4
refactor(analytics): use typed request models in Mixpanel client
fulleni Dec 17, 2025
68a3275
feat(analytics): implement all database queries and trend calculation
fulleni Dec 17, 2025
4034897
docs(README): enhance documentation with analytics engine and archite…
fulleni Dec 17, 2025
7243e95
build(serialization): sync
fulleni Dec 17, 2025
894f197
refactor(analytics): extract database query logic to AnalyticsQueryBu…
fulleni Dec 17, 2025
efbf13f
feat(dependencies): add report repository to app dependencies
fulleni Dec 17, 2025
ea8e97b
refactor(analytics): enhance Mixpanel request models
fulleni Dec 17, 2025
a0fb554
refactor(analytics): enhance documentation and prepare for local data…
fulleni Dec 17, 2025
87b0c58
feat(analytics): add engagement and app review repositories to Analyt…
fulleni Dec 17, 2025
0f1eeb5
style(analytics): add missing semicolons in MixpanelTimeUnit enum
fulleni Dec 17, 2025
f61e2ed
refactor(analytics): reorganize and simplify database queries
fulleni Dec 17, 2025
a80c9b5
feat(analytics): add query builder for database metrics
fulleni Dec 17, 2025
d6c7eaf
chore(core): update core repository ref to eeb2e42
fulleni Dec 17, 2025
b5be136
style: format
fulleni Dec 17, 2025
68a370e
build(serialization): sync
fulleni Dec 17, 2025
93eaae7
chore: misc
fulleni Dec 17, 2025
5a5793a
chore: barrel
fulleni Dec 17, 2025
6ddd813
refactor(analytics): improve database query handling and repo selection
fulleni Dec 17, 2025
64b2e9d
feat(analytics): add pipelines for ranking sources and topics by foll…
fulleni Dec 17, 2025
c8ed729
feat(analytics): implement new database metrics and improve date hand…
fulleni Dec 17, 2025
c257ff5
feat(analytics): add support for calculated metrics and new database …
fulleni Dec 17, 2025
de4099a
refactor(analyticsref): update content metrics for ChartCard
fulleni Dec 17, 2025
cdf8b87
refactor(analytics): replace follower metrics with new distributions
fulleni Dec 17, 2025
8b136c4
fix(analytics): include 'reportsByReason' metric in report repository
fulleni Dec 17, 2025
5bde2fd
feat(analytics): add logging for database analytics pipelines
fulleni Dec 17, 2025
7152464
fix(analytics): correct label formatting and pass client to calculate…
fulleni Dec 17, 2025
a7adc93
chore: update core repository ref in pubspec.yaml
fulleni Dec 17, 2025
e6491bb
refactor(analytics): update ChartCardId enum values to camelCase
fulleni Dec 17, 2025
ea36204
style: format
fulleni Dec 17, 2025
c4a1c6f
refactor(database): remove analytics placeholder seeding
fulleni Dec 18, 2025
ff38821
feat(database): ensure analytics indexes for collections
fulleni Dec 18, 2025
5ad27a8
docs(env): update analytics providers section and reorder configurati…
fulleni Dec 18, 2025
6b75916
fix(database): use runCommand for creating indexes to prevent unique …
fulleni Dec 18, 2025
c2cd4ff
chore: update dep version
fulleni Dec 18, 2025
d8c99ef
build(serialization): sync
fulleni Dec 18, 2025
a3d0457
refactor(analytics): enhance Google Analytics request model
fulleni Dec 18, 2025
dda19ec
fix(analytics): add JSON serialization for Mixpanel requests
fulleni Dec 18, 2025
9cda15e
fix(analytics): add missing metric query for content topics engagement
fulleni Dec 18, 2025
17e19a1
feat(analytics): add topic engagement query and improve source status…
fulleni Dec 18, 2025
015c8d2
refactor(analytics): improve MixpanelDataClient testability and modul…
fulleni Dec 18, 2025
de79c2e
test(analytics): add value equality tests for analytics queries
fulleni Dec 18, 2025
44e3df6
test(analytics): add unit tests for Google Analytics request models
fulleni Dec 18, 2025
0507441
test(analytics): add Google Analytics response model tests
fulleni Dec 18, 2025
1431cc9
test(analytics): add unit tests for Mixpanel request models
fulleni Dec 18, 2025
f8e0b7a
test(analytics): add unit tests for MixpanelResponse model
fulleni Dec 18, 2025
bde0d94
test(analytics): add unit tests for AnalyticsMetricMapper
fulleni Dec 18, 2025
718f066
test(analytics): add AnalyticsSyncService unit tests
fulleni Dec 18, 2025
906e77e
test(analytics): add unit tests for MixpanelDataClient
fulleni Dec 18, 2025
dbc2ce1
test(analytics): add unit tests for AnalyticsQueryBuilder
fulleni Dec 18, 2025
9a06d5b
docs(README): update code coverage percentage
fulleni Dec 18, 2025
892523f
feat(analytics): add integration test for AnalyticsSyncService
fulleni Dec 18, 2025
6afb629
feat(config): add support for optional test database connection
fulleni Dec 18, 2025
04da1e0
refactor(analytics): make HttpClient configurable in GoogleAnalyticsD…
fulleni Dec 18, 2025
a15185e
style: format
fulleni Dec 18, 2025
3732d85
refactor(analytics): remove unused Mixpanel credentials
fulleni Dec 18, 2025
d01fef6
feat(analytics): inject http client into google analytics data client
fulleni Dec 18, 2025
42e0317
test(analytics): remove integration test file
fulleni Dec 18, 2025
2d1bec8
fix(analytics): provide default value for dimensionValues in GARow
fulleni Dec 18, 2025
12c238d
test(analytics): add unit tests for GoogleAnalyticsDataClient
fulleni Dec 18, 2025
0f69ad4
test(analytics): add unit tests for empty API responses in MixpanelDa…
fulleni Dec 18, 2025
adfa018
build(serialization): sync
fulleni Dec 18, 2025
58d0dc7
refactor(config): remove test database URL configuration
fulleni Dec 18, 2025
04cf956
docs: update code coverage badge in README
fulleni Dec 18, 2025
038aae9
style(models): reorder required and not required parameters
fulleni Dec 18, 2025
12f131d
test(analytics): improve type safety in Google Analytics tests
fulleni Dec 18, 2025
bbe05fb
test(analytics): fix type warnings in mixpanel data client tests
fulleni Dec 18, 2025
1111896
docs(README): enhance analytics engine description and remove high-pe…
fulleni Dec 18, 2025
09bc280
refactor(analytics): remove unnecessary null-aware operator
fulleni Dec 18, 2025
28a633a
fix(config): remove null safety from AnalyticsSyncService
fulleni Dec 18, 2025
989c1bc
fix(analytics): update metric names to match backend changes
fulleni Dec 18, 2025
b45e17e
refactor(analytics): update database metric names and categorization
fulleni Dec 18, 2025
e1aa67e
refactor(analytics): improve metric handling and repository lookup
fulleni Dec 18, 2025
7fa9d39
test(analytics): update metric names in query builder tests
fulleni Dec 18, 2025
9cc25de
refactor(database): improve remote config seeding and sanitization
fulleni Dec 18, 2025
dec5fb3
feat(registry): add analytics card data models
fulleni Dec 18, 2025
c4c4186
fix(analytics): improve error handling in data aggregation
fulleni Dec 18, 2025
97ab01e
fix(analytics): prevent crashes in GoogleAnalyticsDataClient
fulleni Dec 18, 2025
46e4ff1
fix(analytics): correct total calculation for Mixpanel segmentations
fulleni Dec 18, 2025
12c9a99
fix(analytics): sum multiple engagement types in Mixpanel data
fulleni Dec 18, 2025
8ea795c
fix(analytics): allow null unit in MixpanelSegmentationRequest
fulleni Dec 18, 2025
c8a2315
- Refactor total request to use MixpanelSegmentationRequest model wit…
fulleni Dec 18, 2025
0f0b412
test(analytics): add missing unit parameter in mixpanel data client test
fulleni Dec 18, 2025
85db269
build(serialization): sync
fulleni Dec 18, 2025
f8a1218
fix(analytics): update GARow constructor to require dimensionValues
fulleni Dec 18, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 21 additions & 2 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -60,9 +60,28 @@ FIREBASE_PRIVATE_KEY="your-firebase-private-key"
ONESIGNAL_APP_ID="your-onesignal-app-id"
ONESIGNAL_REST_API_KEY="your-onesignal-rest-api-key"

# -----------------------------------------------------------------------------
# SECTION 4: ANALYTICS PROVIDERS (CONDITIONALLY REQUIRED)
# Provide credentials for analytics providers to enable dashboard metrics.
# The server will start without these, but the analytics sync worker will skip them.
# -----------------------------------------------------------------------------

# --- Google Analytics (via Firebase) ---
# The ID of your Google Analytics 4 property.
# Note: This requires the Firebase credentials from Section 3 to be configured
# as it uses the same authentication mechanism.
GOOGLE_ANALYTICS_PROPERTY_ID="your-ga4-property-id"

# --- Mixpanel ---
# The Project ID for your Mixpanel project.
MIXPANEL_PROJECT_ID="your-mixpanel-project-id"
# The username for your Mixpanel service account.
MIXPANEL_SERVICE_ACCOUNT_USERNAME="your-mixpanel-service-account-username"
# The secret for your Mixpanel service account.
MIXPANEL_SERVICE_ACCOUNT_SECRET="your-mixpanel-service-account-secret"

# -----------------------------------------------------------------------------
# SECTION 4: API SECURITY & RATE LIMITING (OPTIONAL)
# SECTION 5: API SECURITY & RATE LIMITING (OPTIONAL)
# Fine-tune security settings. Defaults are provided if these are not set.
# -----------------------------------------------------------------------------

Expand All @@ -80,7 +99,7 @@ ONESIGNAL_REST_API_KEY="your-onesignal-rest-api-key"


# -----------------------------------------------------------------------------
# SECTION 5: ADVANCED & MISCELLANEOUS CONFIGURATION (OPTIONAL)
# SECTION 6: ADVANCED & MISCELLANEOUS CONFIGURATION (OPTIONAL)
# -----------------------------------------------------------------------------

# The duration for which a JWT is valid, in hours. Defaults to 720 (30 days).
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/main.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -16,4 +16,4 @@ jobs:
build:
uses: VeryGoodOpenSource/very_good_workflows/.github/workflows/dart_package.yml@v1
with:
min_coverage: 0
min_coverage: 40
36 changes: 34 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
</div>

<p align="center">
<img src="https://img.shields.io/badge/coverage-_%25-red?style=for-the-badge" alt="coverage">
<img src="https://img.shields.io/badge/coverage-53%25-green?style=for-the-badge" alt="coverage">
<a href="https://flutter-news-app-full-source-code.github.io/docs/api-server/local-setup/"><img src="https://img.shields.io/badge/DOCUMENTATION-READ-slategray?style=for-the-badge" alt="Documentation: Read"></a>
</p>
<p align="center">
Expand Down Expand Up @@ -107,11 +107,43 @@ A complete, multi-provider notification engine empowers you to engage users with

</details>

<details>
<summary><strong>📊 Insightful Analytics Engine</strong></summary>

### 📈 A Unified Business Intelligence Engine
A complete, multi-provider analytics engine that transforms raw data from both external services and your own application database into insightful, aggregated metrics for your dashboard.
- **Dual-Source ETL:** A standalone worker process runs on a schedule to perform a full Extract, Transform, and Load (ETL) operation. It pulls behavioral data from your chosen analytics provider (Google Analytics or Mixpanel) and combines it with operational data by running direct, complex aggregations against the application's own database.
- **High-Performance Dashboard:** The web dashboard reads this pre-aggregated data, resulting in near-instant load times for all analytics charts and metrics. This architecture avoids slow, direct, on-the-fly queries from the client to the analytics provider.
- **Provider-Agnostic Design:** The engine is built on a provider-agnostic interface. You can switch between Google Analytics and Mixpanel via a simple configuration change, without altering any code.
- **Extensible & Scalable:** Adding new charts or KPIs is as simple as defining a new mapping. The system is designed to be easily extended to track new metrics as your application evolves.
> **Your Advantage:** Get a complete, production-grade BI pipeline out of the box. Deliver a fast, responsive dashboard and gain a holistic view of your business by combining user behavior analytics with real-time operational metrics—a capability that external analytics tools alone cannot provide.

</details>

<details>
<summary><strong>🏗️ Architecture & Infrastructure</strong></summary>

### 🚀 High-Performance by Design
Built on a modern, minimalist foundation to ensure low latency and excellent performance.
- **Dart Frog Core:** Leverages the high-performance Dart Frog framework for a fast, efficient, and scalable backend.
- **Clean, Layered Architecture:** A strict separation of concerns into distinct layers makes the codebase clean, maintainable, and easy to reason about.
> **Your Advantage:** Your backend is built on a solid, modern foundation that is both powerful and a pleasure to work with, reducing maintenance overhead.

---

### 🔌 Extensible & Unlocked
The entire application is designed with a robust dependency injection system, giving you the freedom to choose your own infrastructure.
- **Swappable Implementations:** Easily swap out core components—like the database, email provider, or file storage service—without rewriting business logic.
> **Your Advantage:** Avoid vendor lock-in and future-proof your application. You have the freedom to adapt and evolve your tech stack as your business needs change.

---

### 🔄 Automated & Traceable Database Migrations
Say goodbye to risky manual database updates. A professional, versioned migration system ensures your database schema evolves safely and automatically.
- **Code-Driven Schema Evolution:** The system automatically applies schema changes to your database on application startup, ensuring consistency across all environments.
- **Traceable to Source:** Each migration is versioned and directly linked to the pull request that initiated it, providing a clear, auditable history of every change.
> **Your Advantage:** Deploy with confidence. This robust system eliminates an entire class of deployment errors, ensuring your data models evolve gracefully and reliably with full traceability.

</details>

## 🔑 Licensing
This `Flutter News App API Server` package is an integral part of the [**Flutter News App Full Source Code Toolkit**](https://github.com/flutter-news-app-full-source-code). For comprehensive details regarding licensing, including trial and commercial options for the entire toolkit, please refer to the main toolkit organization page.
Expand Down
2 changes: 2 additions & 0 deletions analysis_options.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ analyzer:
one_member_abstracts: ignore
cascade_invocations: ignore
cast_nullable_to_non_nullable: ignore
specify_nonobvious_property_types: ignore
unnecessary_null_checks: ignore
exclude:
- build/**
linter:
Expand Down
35 changes: 35 additions & 0 deletions bin/analytics_sync_worker.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import 'dart:io';

import 'package:flutter_news_app_api_server_full_source_code/src/config/app_dependencies.dart';
import 'package:flutter_news_app_api_server_full_source_code/src/services/analytics/analytics.dart';
import 'package:logging/logging.dart';

/// The main entry point for the standalone Analytics Sync Worker process.
///
/// This script initializes application dependencies, retrieves the
/// [AnalyticsSyncService], and executes its `run()` method to perform the
/// periodic data synchronization.
///
/// This executable can be compiled into a native binary and run by a scheduler
/// (e.g., a cron job) to automate the analytics data pipeline.
Future<void> main(List<String> args) async {
// Configure logger for console output.
Logger.root.level = Level.ALL;
Logger.root.onRecord.listen((record) {
// ignore: avoid_print
print('${record.level.name}: ${record.time}: ${record.message}');
if (record.error != null) {
// ignore: avoid_print
print(' ERROR: ${record.error}');
}
if (record.stackTrace != null) {
// ignore: avoid_print
print(' STACK TRACE: ${record.stackTrace}');
}
});

await AppDependencies.instance.init();
await AppDependencies.instance.analyticsSyncService.run();
await AppDependencies.instance.dispose();
exit(0);
}
102 changes: 94 additions & 8 deletions lib/src/config/app_dependencies.dart
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,10 @@ import 'package:email_sendgrid/email_sendgrid.dart';
import 'package:flutter_news_app_api_server_full_source_code/src/config/environment_config.dart';
import 'package:flutter_news_app_api_server_full_source_code/src/database/migrations/all_migrations.dart';
import 'package:flutter_news_app_api_server_full_source_code/src/rbac/permission_service.dart';
import 'package:flutter_news_app_api_server_full_source_code/src/services/analytics/analytics.dart';
import 'package:flutter_news_app_api_server_full_source_code/src/services/auth_service.dart';
import 'package:flutter_news_app_api_server_full_source_code/src/services/auth_token_service.dart';
import 'package:flutter_news_app_api_server_full_source_code/src/services/country_query_service.dart';
import 'package:flutter_news_app_api_server_full_source_code/src/services/dashboard_summary_service.dart';
import 'package:flutter_news_app_api_server_full_source_code/src/services/database_migration_service.dart';
import 'package:flutter_news_app_api_server_full_source_code/src/services/database_seeding_service.dart';
import 'package:flutter_news_app_api_server_full_source_code/src/services/default_user_action_limit_service.dart';
Expand Down Expand Up @@ -72,19 +72,22 @@ class AppDependencies {
pushNotificationDeviceRepository;
late final DataRepository<RemoteConfig> remoteConfigRepository;
late final DataRepository<InAppNotification> inAppNotificationRepository;
late final DataRepository<KpiCardData> kpiCardDataRepository;
late final DataRepository<ChartCardData> chartCardDataRepository;
late final DataRepository<RankedListCardData> rankedListCardDataRepository;

late final DataRepository<Engagement> engagementRepository;
late final DataRepository<Report> reportRepository;
late final DataRepository<AppReview> appReviewRepository;
late final EmailRepository emailRepository;

// Services
late final AnalyticsSyncService analyticsSyncService;
late final DatabaseMigrationService databaseMigrationService;
late final TokenBlacklistService tokenBlacklistService;
late final AuthTokenService authTokenService;
late final VerificationCodeStorageService verificationCodeStorageService;
late final AuthService authService;
late final DashboardSummaryService dashboardSummaryService;
late final PermissionService permissionService;
late final UserActionLimitService userActionLimitService;
late final RateLimitService rateLimitService;
Expand Down Expand Up @@ -232,6 +235,24 @@ class AppDependencies {
logger: Logger('DataMongodb<InAppNotification>'),
);

final kpiCardDataClient = DataMongodb<KpiCardData>(
connectionManager: _mongoDbConnectionManager,
modelName: 'kpi_card_data',
fromJson: KpiCardData.fromJson,
toJson: (item) => item.toJson(),
);
final chartCardDataClient = DataMongodb<ChartCardData>(
connectionManager: _mongoDbConnectionManager,
modelName: 'chart_card_data',
fromJson: ChartCardData.fromJson,
toJson: (item) => item.toJson(),
);
final rankedListCardDataClient = DataMongodb<RankedListCardData>(
connectionManager: _mongoDbConnectionManager,
modelName: 'ranked_list_card_data',
fromJson: RankedListCardData.fromJson,
toJson: (item) => item.toJson(),
);
_log.info('Initialized data client for InAppNotification.');

final engagementClient = DataMongodb<Engagement>(
Expand Down Expand Up @@ -349,6 +370,13 @@ class AppDependencies {
engagementRepository = DataRepository(dataClient: engagementClient);
reportRepository = DataRepository(dataClient: reportClient);
appReviewRepository = DataRepository(dataClient: appReviewClient);
kpiCardDataRepository = DataRepository(dataClient: kpiCardDataClient);
chartCardDataRepository = DataRepository(
dataClient: chartCardDataClient,
);
rankedListCardDataRepository = DataRepository(
dataClient: rankedListCardDataClient,
);

// Configure the HTTP client for SendGrid.
// The HttpClient's AuthInterceptor will use the tokenProvider to add
Expand Down Expand Up @@ -394,11 +422,6 @@ class AppDependencies {
userContentPreferencesRepository: userContentPreferencesRepository,
log: Logger('AuthService'),
);
dashboardSummaryService = DashboardSummaryService(
headlineRepository: headlineRepository,
topicRepository: topicRepository,
sourceRepository: sourceRepository,
);
userActionLimitService = DefaultUserActionLimitService(
remoteConfigRepository: remoteConfigRepository,
engagementRepository: engagementRepository,
Expand All @@ -424,6 +447,69 @@ class AppDependencies {
log: Logger('DefaultPushNotificationService'),
);

// --- Analytics Services ---
final gaPropertyId = EnvironmentConfig.googleAnalyticsPropertyId;
final mpProjectId = EnvironmentConfig.mixpanelProjectId;
final mpUser = EnvironmentConfig.mixpanelServiceAccountUsername;
final mpSecret = EnvironmentConfig.mixpanelServiceAccountSecret;

GoogleAnalyticsDataClient? googleAnalyticsClient;
if (gaPropertyId != null && firebaseAuthenticator != null) {
final googleAnalyticsHttpClient = HttpClient(
baseUrl: 'https://analyticsdata.googleapis.com/v1beta',
tokenProvider: firebaseAuthenticator!.getAccessToken,
logger: Logger('GoogleAnalyticsHttpClient'),
);

googleAnalyticsClient = GoogleAnalyticsDataClient(
headlineRepository: headlineRepository,
propertyId: gaPropertyId,
firebaseAuthenticator: firebaseAuthenticator!,
log: Logger('GoogleAnalyticsDataClient'),
httpClient: googleAnalyticsHttpClient,
);
} else {
_log.warning(
'Google Analytics client could not be initialized due to missing '
'property ID or Firebase authenticator.',
);
}

MixpanelDataClient? mixpanelClient;
if (mpProjectId != null && mpUser != null && mpSecret != null) {
mixpanelClient = MixpanelDataClient(
headlineRepository: headlineRepository,
projectId: mpProjectId,
serviceAccountUsername: mpUser,
serviceAccountSecret: mpSecret,
log: Logger('MixpanelDataClient'),
);
} else {
_log.warning(
'Mixpanel client could not be initialized due to missing credentials.',
);
}

final analyticsMetricMapper = AnalyticsMetricMapper();

analyticsSyncService = AnalyticsSyncService(
remoteConfigRepository: remoteConfigRepository,
kpiCardRepository: kpiCardDataRepository,
chartCardRepository: chartCardDataRepository,
rankedListCardRepository: rankedListCardDataRepository,
userRepository: userRepository,
topicRepository: topicRepository,
sourceRepository: sourceRepository,
reportRepository: reportRepository,
headlineRepository: headlineRepository,
googleAnalyticsClient: googleAnalyticsClient,
mixpanelClient: mixpanelClient,
analyticsMetricMapper: analyticsMetricMapper,
engagementRepository: engagementRepository,
appReviewRepository: appReviewRepository,
log: Logger('AnalyticsSyncService'),
);

_log.info('Application dependencies initialized successfully.');
// Signal that initialization has completed successfully.
_initCompleter!.complete();
Expand Down Expand Up @@ -459,7 +545,7 @@ class AppDependencies {
await _mongoDbConnectionManager.close();
tokenBlacklistService.dispose();
rateLimitService.dispose();
countryQueryService.dispose(); // Dispose the new service
countryQueryService.dispose();

// Reset the completer to allow for re-initialization (e.g., in tests).
_initCompleter = null;
Expand Down
15 changes: 15 additions & 0 deletions lib/src/config/environment_config.dart
Original file line number Diff line number Diff line change
Expand Up @@ -219,4 +219,19 @@ abstract final class EnvironmentConfig {
///
/// The value is read from the `ONESIGNAL_REST_API_KEY` environment variable, if available.
static String? get oneSignalRestApiKey => _getEnv('ONESIGNAL_REST_API_KEY');

/// Retrieves the Google Analytics Property ID from the environment.
static String? get googleAnalyticsPropertyId =>
_getEnv('GOOGLE_ANALYTICS_PROPERTY_ID');

/// Retrieves the Mixpanel Project ID from the environment.
static String? get mixpanelProjectId => _getEnv('MIXPANEL_PROJECT_ID');

/// Retrieves the Mixpanel Service Account Username from the environment.
static String? get mixpanelServiceAccountUsername =>
_getEnv('MIXPANEL_SERVICE_ACCOUNT_USERNAME');

/// Retrieves the Mixpanel Service Account Secret from the environment.
static String? get mixpanelServiceAccountSecret =>
_getEnv('MIXPANEL_SERVICE_ACCOUNT_SECRET');
}
5 changes: 5 additions & 0 deletions lib/src/models/analytics/analytics.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export 'analytics_query.dart';
export 'google_analytics_request.dart';
export 'google_analytics_response.dart';
export 'mixpanel_request.dart';
export 'mixpanel_response.dart';
60 changes: 60 additions & 0 deletions lib/src/models/analytics/analytics_query.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import 'package:core/core.dart';

/// A sealed class representing a structured, provider-agnostic analytics query.
///
/// This replaces the fragile pattern of passing primitive strings for metrics
/// and dimensions, centralizing query definitions into type-safe objects.
sealed class AnalyticsQuery {
/// {@macro analytics_query}
const AnalyticsQuery();
}

/// A sealed class for queries that return a numeric metric value, such as
/// a total count or a time series.
sealed class MetricQuery extends AnalyticsQuery {
/// {@macro metric_query}
const MetricQuery();
}

/// A query for a simple event count.
///
/// This is used when the metric is the count of a specific [AnalyticsEvent].
class EventCountQuery extends MetricQuery {
/// {@macro event_count_query}
const EventCountQuery({required this.event});

/// The core, type-safe event from the shared [AnalyticsEvent] enum.
final AnalyticsEvent event;
}

/// A query for a standard, provider-defined metric (e.g., 'activeUsers').
///
/// This is used for metrics that have a built-in name in the provider's API.
class StandardMetricQuery extends MetricQuery {
/// {@macro standard_metric_query}
const StandardMetricQuery({required this.metric});

/// The provider-specific name for a standard metric.
final String metric;
}

/// A query for a ranked list of items.
///
/// This is used to get a "Top N" list, such as most viewed headlines.
class RankedListQuery extends AnalyticsQuery {
/// {@macro ranked_list_query}
const RankedListQuery({
required this.event,
required this.dimension,
this.limit = 10,
});

/// The event to count for ranking (e.g., `contentViewed`).
final AnalyticsEvent event;

/// The property/dimension to group by (e.g., `contentId`).
final String dimension;

/// The number of items to return in the ranked list.
final int limit;
}
Loading
Loading