From 58fa3a67905ce1e853505018d0ac85c0cf1b511c Mon Sep 17 00:00:00 2001 From: Manus AI Date: Sun, 8 Feb 2026 21:13:29 -0500 Subject: [PATCH 1/6] feat: Add universe-based analytics events to core services - Add analytics event triggering to crud service (delete, bulk actions, import) - Add analytics event triggering to resource-action service (create, update, delete) - Add session analytics events (initialized, terminated with duration) - Emit both generic (resource.created) and specific (order.created) events - Use dot notation naming convention for all events - Non-breaking changes, events are optional and don't affect existing functionality --- addon/services/crud.js | 37 +++++++++++++++++++++++++++++++ addon/services/resource-action.js | 31 ++++++++++++++++++++++++++ addon/services/session.js | 16 +++++++++++++ 3 files changed, 84 insertions(+) diff --git a/addon/services/crud.js b/addon/services/crud.js index 2e7beff1..1fa7489b 100644 --- a/addon/services/crud.js +++ b/addon/services/crud.js @@ -17,6 +17,7 @@ export default class CrudService extends Service { @service notifications; @service store; @service currentUser; + @service universe; /** * Generic deletion modal with options @@ -43,6 +44,10 @@ export default class CrudService extends Service { try { const response = await model.destroyRecord(); this.notifications.success(successNotification); + + // Trigger analytics events + this._triggerResourceEvent('deleted', model); + if (typeof options.onSuccess === 'function') { options.onSuccess(model); } @@ -161,6 +166,13 @@ export default class CrudService extends Service { ); this.notifications.success(response.message ?? successMessage); + + // Trigger bulk action event + if (verb === 'delete') { + selected.forEach(model => this._triggerResourceEvent('deleted', model)); + } + this.universe.trigger('resource.bulk_action', verb, selected, firstModel); + if (typeof options.onSuccess === 'function') { options.onSuccess(selected); } @@ -248,6 +260,25 @@ export default class CrudService extends Service { * @param {Object} [options={}] * @memberof CrudService */ + /** + * Helper method to trigger both generic and specific resource events + * + * @private + * @param {String} action - The action performed (created, updated, deleted, etc.) + * @param {Model} model - The Ember Data model + */ + _triggerResourceEvent(action, model) { + // Trigger generic resource event + this.universe.trigger(`resource.${action}`, model); + + // Trigger specific model event (e.g., order.created, vehicle.updated) + const modelName = getModelName(model); + if (modelName) { + const specificModelName = dasherize(modelName).replace(/-/g, '_'); + this.universe.trigger(`${specificModelName}.${action}`, model); + } + } + @action import(modelName, options = {}) { // always lowercase modelname modelName = modelName.toLowerCase(); @@ -337,6 +368,12 @@ export default class CrudService extends Service { try { const response = await this.fetch.post(importEndpoint, { files }, fetchOptions); + + // Trigger import event + this.universe.trigger('resource.imported', modelName, response, files); + const specificModelName = dasherize(modelName).replace(/-/g, '_'); + this.universe.trigger(`${specificModelName}.imported`, response, files); + if (typeof options.onImportCompleted === 'function') { options.onImportCompleted(response, files); } diff --git a/addon/services/resource-action.js b/addon/services/resource-action.js index f79d1e25..a2bcbcd6 100644 --- a/addon/services/resource-action.js +++ b/addon/services/resource-action.js @@ -29,6 +29,7 @@ export default class ResourceActionService extends Service { @service abilities; @service tableContext; @service resourceContextPanel; + @service universe; /** * Getter for router, attempt to use hostRouter if from engine @@ -299,6 +300,9 @@ export default class ResourceActionService extends Service { }) ); + // Trigger analytics events + this._triggerResourceEvent('created', record); + if (options.refresh) { this.refresh(); } @@ -329,6 +333,9 @@ export default class ResourceActionService extends Service { }) ); + // Trigger analytics events + this._triggerResourceEvent('updated', record); + if (options.refresh) { this.refresh(); } @@ -362,6 +369,9 @@ export default class ResourceActionService extends Service { }) ); + // Trigger analytics events + this._triggerResourceEvent(isNew ? 'created' : 'updated', record); + if (options.refresh) { this.refresh(); } @@ -409,6 +419,9 @@ export default class ResourceActionService extends Service { }) ); + // Trigger analytics events + this._triggerResourceEvent('deleted', record); + if (options.refresh) { this.refresh(); } @@ -424,6 +437,24 @@ export default class ResourceActionService extends Service { } } + /** + * Helper method to trigger both generic and specific resource events + * + * @private + * @param {String} action - The action performed (created, updated, deleted, etc.) + * @param {Model} record - The Ember Data model + */ + _triggerResourceEvent(action, record) { + // Trigger generic resource event + this.universe.trigger(`resource.${action}`, record); + + // Trigger specific model event using the service's modelName + if (this.modelName) { + const specificModelName = this.modelName.replace(/-/g, '_'); + this.universe.trigger(`${specificModelName}.${action}`, record); + } + } + /** * Searches for records with debouncing. * Uses ember-concurrency for async handling with restartable behavior. diff --git a/addon/services/session.js b/addon/services/session.js index f78f4fb7..4aaa5a04 100644 --- a/addon/services/session.js +++ b/addon/services/session.js @@ -10,6 +10,7 @@ export default class SessionService extends SimpleAuthSessionService { @service currentUser; @service fetch; @service notifications; + @service universe; /** * Set where to transition to @@ -18,6 +19,13 @@ export default class SessionService extends SimpleAuthSessionService { */ @tracked redirectTo = 'console'; + /** + * Track session start time for duration calculation + * + * @var {Date} + */ + @tracked sessionStartTime = null; + /** * If session is onboarding a user. * @@ -89,6 +97,10 @@ export default class SessionService extends SimpleAuthSessionService { return this.invalidateWithLoader('Session authentication failed...'); } + // Track session initialization + this.sessionStartTime = new Date(); + this.universe.trigger('session.initialized', user, user.organization); + return user; } catch (error) { await this.invalidateWithLoader(getWithDefault(error, 'message', 'Session authentication failed...')); @@ -155,6 +167,10 @@ export default class SessionService extends SimpleAuthSessionService { * @return {Promise} */ invalidateWithLoader(loadingMessage = 'Ending session...') { + // Track session termination + const sessionDuration = this.sessionStartTime ? Math.round((new Date() - this.sessionStartTime) / 1000) : null; + this.universe.trigger('session.terminated', this.currentUser.user, sessionDuration); + // if loader node is open already just invalidate if (this.isLoaderNodeOpen === true) { return this.session.invalidate(); From 97960c06ebd23e0d748aa8f5de80f665787b2ac0 Mon Sep 17 00:00:00 2001 From: Manus AI Date: Sun, 8 Feb 2026 21:23:37 -0500 Subject: [PATCH 2/6] fix: Add export tracking and remove incorrect session tracking - Add resource.exported and {model}.exported events to crud service export method - Remove incorrect session tracking from session service (was in wrong place) - Session events should be tracked via current-user service's existing 'user.loaded' event - Current-user service already extends Evented and triggers 'user.loaded' on lines 72 and 99 The current-user service is the proper place for user/session lifecycle events since it: 1. Already extends Evented mixin 2. Already triggers 'user.loaded' event when user is authenticated 3. Is called by session.loadCurrentUser() and session.promiseCurrentUser() 4. Has access to user and organization data For logout/session termination tracking, consumers should subscribe to ember-simple-auth's 'invalidationSucceeded' event or add a 'user.logout' event to current-user service. --- addon/services/crud.js | 5 +++++ addon/services/session.js | 16 ---------------- 2 files changed, 5 insertions(+), 16 deletions(-) diff --git a/addon/services/crud.js b/addon/services/crud.js index 1fa7489b..52a2d50b 100644 --- a/addon/services/crud.js +++ b/addon/services/crud.js @@ -236,6 +236,11 @@ export default class CrudService extends Service { } ) .then(() => { + // Trigger export event + this.universe.trigger('resource.exported', modelName, format, exportParams); + const specificModelName = dasherize(modelName).replace(/-/g, '_'); + this.universe.trigger(`${specificModelName}.exported`, format, exportParams); + later( this, () => { diff --git a/addon/services/session.js b/addon/services/session.js index 4aaa5a04..f78f4fb7 100644 --- a/addon/services/session.js +++ b/addon/services/session.js @@ -10,7 +10,6 @@ export default class SessionService extends SimpleAuthSessionService { @service currentUser; @service fetch; @service notifications; - @service universe; /** * Set where to transition to @@ -19,13 +18,6 @@ export default class SessionService extends SimpleAuthSessionService { */ @tracked redirectTo = 'console'; - /** - * Track session start time for duration calculation - * - * @var {Date} - */ - @tracked sessionStartTime = null; - /** * If session is onboarding a user. * @@ -97,10 +89,6 @@ export default class SessionService extends SimpleAuthSessionService { return this.invalidateWithLoader('Session authentication failed...'); } - // Track session initialization - this.sessionStartTime = new Date(); - this.universe.trigger('session.initialized', user, user.organization); - return user; } catch (error) { await this.invalidateWithLoader(getWithDefault(error, 'message', 'Session authentication failed...')); @@ -167,10 +155,6 @@ export default class SessionService extends SimpleAuthSessionService { * @return {Promise} */ invalidateWithLoader(loadingMessage = 'Ending session...') { - // Track session termination - const sessionDuration = this.sessionStartTime ? Math.round((new Date() - this.sessionStartTime) / 1000) : null; - this.universe.trigger('session.terminated', this.currentUser.user, sessionDuration); - // if loader node is open already just invalidate if (this.isLoaderNodeOpen === true) { return this.session.invalidate(); From d6fc8a3f793408fd501095007d5a457fb418e14c Mon Sep 17 00:00:00 2001 From: "Ronald A. Richardson" Date: Tue, 10 Feb 2026 11:45:28 +0800 Subject: [PATCH 3/6] fixed linter --- addon/services/crud.js | 18 +++++++++--------- addon/services/resource-action.js | 2 +- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/addon/services/crud.js b/addon/services/crud.js index 52a2d50b..dca8c89b 100644 --- a/addon/services/crud.js +++ b/addon/services/crud.js @@ -44,10 +44,10 @@ export default class CrudService extends Service { try { const response = await model.destroyRecord(); this.notifications.success(successNotification); - + // Trigger analytics events this._triggerResourceEvent('deleted', model); - + if (typeof options.onSuccess === 'function') { options.onSuccess(model); } @@ -166,13 +166,13 @@ export default class CrudService extends Service { ); this.notifications.success(response.message ?? successMessage); - + // Trigger bulk action event if (verb === 'delete') { - selected.forEach(model => this._triggerResourceEvent('deleted', model)); + selected.forEach((model) => this._triggerResourceEvent('deleted', model)); } this.universe.trigger('resource.bulk_action', verb, selected, firstModel); - + if (typeof options.onSuccess === 'function') { options.onSuccess(selected); } @@ -240,7 +240,7 @@ export default class CrudService extends Service { this.universe.trigger('resource.exported', modelName, format, exportParams); const specificModelName = dasherize(modelName).replace(/-/g, '_'); this.universe.trigger(`${specificModelName}.exported`, format, exportParams); - + later( this, () => { @@ -275,7 +275,7 @@ export default class CrudService extends Service { _triggerResourceEvent(action, model) { // Trigger generic resource event this.universe.trigger(`resource.${action}`, model); - + // Trigger specific model event (e.g., order.created, vehicle.updated) const modelName = getModelName(model); if (modelName) { @@ -373,12 +373,12 @@ export default class CrudService extends Service { try { const response = await this.fetch.post(importEndpoint, { files }, fetchOptions); - + // Trigger import event this.universe.trigger('resource.imported', modelName, response, files); const specificModelName = dasherize(modelName).replace(/-/g, '_'); this.universe.trigger(`${specificModelName}.imported`, response, files); - + if (typeof options.onImportCompleted === 'function') { options.onImportCompleted(response, files); } diff --git a/addon/services/resource-action.js b/addon/services/resource-action.js index a2bcbcd6..62827c43 100644 --- a/addon/services/resource-action.js +++ b/addon/services/resource-action.js @@ -447,7 +447,7 @@ export default class ResourceActionService extends Service { _triggerResourceEvent(action, record) { // Trigger generic resource event this.universe.trigger(`resource.${action}`, record); - + // Trigger specific model event using the service's modelName if (this.modelName) { const specificModelName = this.modelName.replace(/-/g, '_'); From bad3418d4c1637800bf10823b297e566aaaa5938 Mon Sep 17 00:00:00 2001 From: Manus AI Date: Tue, 10 Feb 2026 20:38:13 -0500 Subject: [PATCH 4/6] feat: Add dedicated analytics service with private # methods - Created centralized analytics service for event tracking - Refactored crud, resource-action, and current-user services to use analytics service - Removed scattered _triggerResourceEvent() methods - Added global service exports via host-services and services - Implemented private methods using native # syntax - Added comprehensive documentation in ANALYTICS_SERVICE.md Benefits: - Single responsibility: One service handles all analytics - Reusable: Any engine can inject and use it - Consistent API: Standardized methods for tracking - Cleaner services: CRUD/resource-action stay focused - Better testability: Can mock/stub analytics service easily --- ANALYTICS_SERVICE.md | 425 ++++++++++++++++++++++++++++++ addon/exports/host-services.js | 1 + addon/exports/services.js | 1 + addon/services/analytics.js | 318 ++++++++++++++++++++++ addon/services/crud.js | 44 +--- addon/services/current-user.js | 7 + addon/services/resource-action.js | 39 +-- app/services/analytics.js | 1 + 8 files changed, 778 insertions(+), 58 deletions(-) create mode 100644 ANALYTICS_SERVICE.md create mode 100644 addon/services/analytics.js create mode 100644 app/services/analytics.js diff --git a/ANALYTICS_SERVICE.md b/ANALYTICS_SERVICE.md new file mode 100644 index 00000000..7025bc0c --- /dev/null +++ b/ANALYTICS_SERVICE.md @@ -0,0 +1,425 @@ +# Analytics Service + +The `analytics` service provides a centralized, analytics-agnostic event tracking system for Fleetbase. It emits standardized events via the `universe` service's event bus, allowing engines (like `internals`) to subscribe and implement their own analytics integrations. + +## Overview + +The analytics service is designed to: + +- **Centralize event emission** - Single source of truth for all analytics events +- **Remain analytics-agnostic** - No vendor-specific code (PostHog, Google Analytics, etc.) +- **Use the universe event bus** - Events are published via `universe.trigger()` +- **Be opt-in** - Services and components must explicitly call tracking methods +- **Enrich events automatically** - Adds user, organization, and timestamp context + +## Architecture + +``` +Service/Component + ↓ +analytics.trackResourceCreated(order) + ↓ +Analytics Service + - Enriches with metadata + - Formats payload + ↓ +universe.trigger('resource.created', ...) +universe.trigger('order.created', ...) + ↓ +Internals (or other engines) + - Listens via universe.on() + - Translates to PostHog/etc +``` + +## Installation + +The analytics service is automatically available in all engines and the console application. It's exported globally via `host-services` and `services`. + +### Injection + +```javascript +import { inject as service } from '@ember/service'; + +export default class MyService extends Service { + @service analytics; + + async createOrder(orderData) { + const order = await this.store.createRecord('order', orderData).save(); + this.analytics.trackResourceCreated(order); + return order; + } +} +``` + +## API Reference + +### Resource Tracking + +#### `trackResourceCreated(resource, props = {})` + +Tracks the creation of a new resource. + +**Parameters:** +- `resource` (Object) - The created Ember Data model +- `props` (Object, optional) - Additional properties to include + +**Events Emitted:** +- `resource.created` (generic) +- `{modelName}.created` (specific, e.g., `order.created`) + +**Example:** +```javascript +this.analytics.trackResourceCreated(order); +// Emits: resource.created, order.created +``` + +#### `trackResourceUpdated(resource, props = {})` + +Tracks the update of an existing resource. + +**Parameters:** +- `resource` (Object) - The updated Ember Data model +- `props` (Object, optional) - Additional properties to include + +**Events Emitted:** +- `resource.updated` (generic) +- `{modelName}.updated` (specific, e.g., `driver.updated`) + +**Example:** +```javascript +this.analytics.trackResourceUpdated(driver); +// Emits: resource.updated, driver.updated +``` + +#### `trackResourceDeleted(resource, props = {})` + +Tracks the deletion of a resource. + +**Parameters:** +- `resource` (Object) - The deleted Ember Data model +- `props` (Object, optional) - Additional properties to include + +**Events Emitted:** +- `resource.deleted` (generic) +- `{modelName}.deleted` (specific, e.g., `vehicle.deleted`) + +**Example:** +```javascript +this.analytics.trackResourceDeleted(vehicle); +// Emits: resource.deleted, vehicle.deleted +``` + +#### `trackResourceImported(modelName, count, props = {})` + +Tracks a bulk import of resources. + +**Parameters:** +- `modelName` (String) - The name of the model being imported +- `count` (Number) - Number of resources imported +- `props` (Object, optional) - Additional properties to include + +**Events Emitted:** +- `resource.imported` + +**Example:** +```javascript +this.analytics.trackResourceImported('contact', 50); +// Emits: resource.imported with count: 50 +``` + +#### `trackResourceExported(modelName, format, params = {}, props = {})` + +Tracks a resource export. + +**Parameters:** +- `modelName` (String) - The name of the model being exported +- `format` (String) - Export format (csv, xlsx, pdf, etc.) +- `params` (Object, optional) - Export parameters/filters +- `props` (Object, optional) - Additional properties to include + +**Events Emitted:** +- `resource.exported` (generic) +- `{modelName}.exported` (specific, e.g., `order.exported`) + +**Example:** +```javascript +this.analytics.trackResourceExported('order', 'csv', { status: 'completed' }); +// Emits: resource.exported, order.exported +``` + +#### `trackBulkAction(verb, resources, props = {})` + +Tracks a bulk action on multiple resources. + +**Parameters:** +- `verb` (String) - The action verb (delete, archive, etc.) +- `resources` (Array) - Array of selected resources +- `props` (Object, optional) - Additional properties to include + +**Events Emitted:** +- `resource.bulk_action` + +**Example:** +```javascript +this.analytics.trackBulkAction('delete', selectedOrders); +// Emits: resource.bulk_action with count and action +``` + +### Session Tracking + +#### `trackUserLoaded(user, organization, props = {})` + +Tracks when the current user is loaded (session initialized). + +**Parameters:** +- `user` (Object) - The user object +- `organization` (Object) - The organization object +- `props` (Object, optional) - Additional properties to include + +**Events Emitted:** +- `user.loaded` + +**Example:** +```javascript +this.analytics.trackUserLoaded(user, organization); +// Emits: user.loaded +``` + +#### `trackSessionTerminated(duration, props = {})` + +Tracks when a user session ends. + +**Parameters:** +- `duration` (Number) - Session duration in seconds +- `props` (Object, optional) - Additional properties to include + +**Events Emitted:** +- `session.terminated` + +**Example:** +```javascript +this.analytics.trackSessionTerminated(3600); +// Emits: session.terminated with duration +``` + +### Custom Events + +#### `trackEvent(eventName, props = {})` + +Tracks a generic custom event. + +**Parameters:** +- `eventName` (String) - The event name (dot notation recommended) +- `props` (Object, optional) - Event properties + +**Events Emitted:** +- `{eventName}` (as specified) + +**Example:** +```javascript +this.analytics.trackEvent('chat.message.sent', { length: 140 }); +// Emits: chat.message.sent +``` + +### Utility Methods + +#### `isEnabled()` + +Checks if analytics tracking is enabled. + +**Returns:** `Boolean` + +**Example:** +```javascript +if (this.analytics.isEnabled()) { + // Tracking is enabled +} +``` + +## Configuration + +The analytics service can be configured via `config/environment.js`: + +```javascript +// config/environment.js +ENV.analytics = { + enabled: true, // Master switch (default: true) + debug: false, // Log events to console (default: false) + enrich: { + user: true, // Add user_id to events (default: true) + organization: true, // Add organization_id to events (default: true) + timestamp: true // Add timestamp to events (default: true) + } +}; +``` + +### Configuration Options + +| Option | Type | Default | Description | +|--------|------|---------|-------------| +| `enabled` | Boolean | `true` | Master switch to enable/disable all tracking | +| `debug` | Boolean | `false` | Log events to console for debugging | +| `enrich.user` | Boolean | `true` | Add `user_id` to all events | +| `enrich.organization` | Boolean | `true` | Add `organization_id` to all events | +| `enrich.timestamp` | Boolean | `true` | Add `timestamp` to all events | + +## Event Naming Convention + +All events use **dot notation** with the following patterns: + +- **Generic resource events:** `resource.{action}` (e.g., `resource.created`) +- **Specific resource events:** `{modelName}.{action}` (e.g., `order.created`) +- **Session events:** `user.loaded`, `session.terminated` +- **Custom events:** Use dot notation (e.g., `chat.message.sent`) + +## Event Properties + +All events are automatically enriched with the following properties (if enabled): + +- `user_id` - Current user's ID +- `organization_id` - Current organization's ID +- `timestamp` - ISO 8601 timestamp + +Resource events also include: + +- `id` - Resource ID +- `model_name` - Model name +- `name` - Resource name (if available) +- `status` - Resource status (if available) +- `type` - Resource type (if available) + +## Usage Examples + +### In a Service + +```javascript +import Service, { inject as service } from '@ember/service'; + +export default class OrderService extends Service { + @service store; + @service analytics; + + async createOrder(orderData) { + const order = this.store.createRecord('order', orderData); + await order.save(); + + // Track the creation + this.analytics.trackResourceCreated(order); + + return order; + } + + async updateOrder(order, updates) { + order.setProperties(updates); + await order.save(); + + // Track the update + this.analytics.trackResourceUpdated(order); + + return order; + } +} +``` + +### In a Component + +```javascript +import Component from '@glimmer/component'; +import { inject as service } from '@ember/service'; +import { action } from '@ember/object'; + +export default class OrderFormComponent extends Component { + @service analytics; + + @action + async saveOrder() { + await this.args.order.save(); + + // Track the save + if (this.args.order.isNew) { + this.analytics.trackResourceCreated(this.args.order); + } else { + this.analytics.trackResourceUpdated(this.args.order); + } + } +} +``` + +### Listening to Events (in Engines) + +```javascript +// In an engine's instance-initializer +export function initialize(owner) { + const universe = owner.lookup('service:universe'); + const posthog = owner.lookup('service:posthog'); + + // Listen to order creation events + universe.on('order.created', (order, properties) => { + posthog.trackEvent('order_created', { + order_id: order.id, + ...properties + }); + }); + + // Listen to all resource creation events + universe.on('resource.created', (resource, properties) => { + posthog.trackEvent('resource_created', properties); + }); +} +``` + +## Best Practices + +1. **Use specific tracking methods** - Prefer `trackResourceCreated()` over `trackEvent()` +2. **Track after success** - Only track events after the operation succeeds +3. **Keep properties minimal** - Only include necessary data +4. **Use dot notation** - For custom event names (e.g., `chat.message.sent`) +5. **Don't track sensitive data** - Avoid passwords, tokens, payment info +6. **Test with debug mode** - Set `analytics.debug: true` in development + +## Testing + +To stub the analytics service in tests: + +```javascript +import { module, test } from 'qunit'; +import { setupTest } from 'ember-qunit'; +import sinon from 'sinon'; + +module('Unit | Service | order', function(hooks) { + setupTest(hooks); + + test('it tracks order creation', async function(assert) { + const service = this.owner.lookup('service:order'); + const analytics = this.owner.lookup('service:analytics'); + + // Stub the tracking method + const trackStub = sinon.stub(analytics, 'trackResourceCreated'); + + await service.createOrder({ name: 'Test Order' }); + + // Assert tracking was called + assert.ok(trackStub.calledOnce); + }); +}); +``` + +## Migration from Direct universe.trigger() + +If you previously used `universe.trigger()` directly: + +**Before:** +```javascript +this.universe.trigger('resource.created', model); +this.universe.trigger('order.created', model); +``` + +**After:** +```javascript +this.analytics.trackResourceCreated(model); +// Automatically emits both events +``` + +## Support + +For questions or issues with the analytics service, please refer to the Fleetbase documentation or contact the development team. diff --git a/addon/exports/host-services.js b/addon/exports/host-services.js index cc8d3f69..91830230 100644 --- a/addon/exports/host-services.js +++ b/addon/exports/host-services.js @@ -19,6 +19,7 @@ export const hostServices = [ 'sidebar', 'dashboard', 'universe', + 'analytics', 'intl', 'abilities', 'language', diff --git a/addon/exports/services.js b/addon/exports/services.js index 0f728bb9..79fa36c5 100644 --- a/addon/exports/services.js +++ b/addon/exports/services.js @@ -21,6 +21,7 @@ export const services = [ 'sidebar', 'dashboard', 'universe', + 'analytics', 'intl', 'abilities', 'language', diff --git a/addon/services/analytics.js b/addon/services/analytics.js new file mode 100644 index 00000000..d52172e9 --- /dev/null +++ b/addon/services/analytics.js @@ -0,0 +1,318 @@ +import Service, { inject as service } from '@ember/service'; +import { getOwner } from '@ember/application'; +import config from 'ember-get-config'; + +/** + * Analytics Service + * + * Provides a centralized, analytics-agnostic event tracking system for Fleetbase. + * This service emits standardized events via the universe service's event bus, + * allowing engines (like internals) to subscribe and implement their own analytics. + * + * @class AnalyticsService + * @extends Service + */ +export default class AnalyticsService extends Service { + @service universe; + @service currentUser; + + /** + * Tracks the creation of a resource + * + * @param {Object} resource - The created resource/model + * @param {Object} [props={}] - Additional properties to include + */ + trackResourceCreated(resource, props = {}) { + const events = this.#getResourceEvents(resource, 'created'); + const properties = this.#enrichProperties({ + ...this.#getSafeProperties(resource), + ...props + }); + + events.forEach(eventName => { + this.#trigger(eventName, resource, properties); + }); + } + + /** + * Tracks the update of a resource + * + * @param {Object} resource - The updated resource/model + * @param {Object} [props={}] - Additional properties to include + */ + trackResourceUpdated(resource, props = {}) { + const events = this.#getResourceEvents(resource, 'updated'); + const properties = this.#enrichProperties({ + ...this.#getSafeProperties(resource), + ...props + }); + + events.forEach(eventName => { + this.#trigger(eventName, resource, properties); + }); + } + + /** + * Tracks the deletion of a resource + * + * @param {Object} resource - The deleted resource/model + * @param {Object} [props={}] - Additional properties to include + */ + trackResourceDeleted(resource, props = {}) { + const events = this.#getResourceEvents(resource, 'deleted'); + const properties = this.#enrichProperties({ + ...this.#getSafeProperties(resource), + ...props + }); + + events.forEach(eventName => { + this.#trigger(eventName, resource, properties); + }); + } + + /** + * Tracks a bulk import of resources + * + * @param {String} modelName - The name of the model being imported + * @param {Number} count - Number of resources imported + * @param {Object} [props={}] - Additional properties to include + */ + trackResourceImported(modelName, count, props = {}) { + const properties = this.#enrichProperties({ + model_name: modelName, + count: count, + ...props + }); + + this.#trigger('resource.imported', modelName, count, properties); + } + + /** + * Tracks a resource export + * + * @param {String} modelName - The name of the model being exported + * @param {String} format - Export format (csv, xlsx, etc.) + * @param {Object} [params={}] - Export parameters/filters + * @param {Object} [props={}] - Additional properties to include + */ + trackResourceExported(modelName, format, params = {}, props = {}) { + const properties = this.#enrichProperties({ + model_name: modelName, + export_format: format, + has_filters: !!(params && Object.keys(params).length > 0), + ...props + }); + + this.#trigger('resource.exported', modelName, format, params, properties); + + // Also trigger model-specific export event + const specificEvent = `${modelName}.exported`; + this.#trigger(specificEvent, modelName, format, params, properties); + } + + /** + * Tracks a bulk action on multiple resources + * + * @param {String} verb - The action verb (delete, archive, etc.) + * @param {Array} resources - Array of selected resources + * @param {Object} [props={}] - Additional properties to include + */ + trackBulkAction(verb, resources, props = {}) { + const firstResource = resources && resources.length > 0 ? resources[0] : null; + const modelName = this.#getModelName(firstResource); + + const properties = this.#enrichProperties({ + action: verb, + count: resources?.length || 0, + model_name: modelName, + ...props + }); + + this.#trigger('resource.bulk_action', verb, resources, firstResource, properties); + } + + /** + * Tracks when the current user is loaded (session initialized) + * + * @param {Object} user - The user object + * @param {Object} organization - The organization object + * @param {Object} [props={}] - Additional properties to include + */ + trackUserLoaded(user, organization, props = {}) { + const properties = this.#enrichProperties({ + user_id: user?.id, + organization_id: organization?.id, + organization_name: organization?.name, + ...props + }); + + this.#trigger('user.loaded', user, organization, properties); + } + + /** + * Tracks when a user session is terminated + * + * @param {Number} duration - Session duration in seconds + * @param {Object} [props={}] - Additional properties to include + */ + trackSessionTerminated(duration, props = {}) { + const properties = this.#enrichProperties({ + session_duration: duration, + ...props + }); + + this.#trigger('session.terminated', duration, properties); + } + + /** + * Tracks a generic custom event + * + * @param {String} eventName - The event name (dot notation) + * @param {Object} [props={}] - Event properties + */ + trackEvent(eventName, props = {}) { + const properties = this.#enrichProperties(props); + this.#trigger(eventName, properties); + } + + /** + * Checks if analytics tracking is enabled + * + * @returns {Boolean} + */ + isEnabled() { + const analyticsConfig = config?.analytics || {}; + return analyticsConfig.enabled !== false; // Enabled by default + } + + // ========================================================================= + // Private Methods (using # syntax) + // ========================================================================= + + /** + * Triggers an event on the universe service + * + * @private + * @param {String} eventName - The event name + * @param {...*} args - Arguments to pass to event listeners + */ + #trigger(eventName, ...args) { + if (!this.isEnabled()) { + return; + } + + if (!this.universe) { + console.warn('[Analytics] Universe service not available'); + return; + } + + // Debug logging if enabled + if (config?.analytics?.debug) { + console.log(`[Analytics] ${eventName}`, args); + } + + this.universe.trigger(eventName, ...args); + } + + /** + * Generates both generic and specific event names for a resource action + * + * @private + * @param {Object} resource - The resource/model + * @param {String} action - The action (created, updated, deleted) + * @returns {Array} Array of event names + */ + #getResourceEvents(resource, action) { + const modelName = this.#getModelName(resource); + return [ + `resource.${action}`, + `${modelName}.${action}` + ]; + } + + /** + * Extracts safe, serializable properties from a resource + * + * @private + * @param {Object} resource - The resource/model + * @returns {Object} Safe properties object + */ + #getSafeProperties(resource) { + if (!resource) { + return {}; + } + + const props = { + id: resource.id, + model_name: this.#getModelName(resource) + }; + + // Add common properties if available + const commonProps = ['name', 'status', 'type', 'slug', 'public_id']; + commonProps.forEach(prop => { + if (resource[prop] !== undefined && resource[prop] !== null) { + props[prop] = resource[prop]; + } + }); + + return props; + } + + /** + * Enriches properties with user, organization, and timestamp context + * + * @private + * @param {Object} props - Base properties + * @returns {Object} Enriched properties + */ + #enrichProperties(props = {}) { + const analyticsConfig = config?.analytics || {}; + const enrichConfig = analyticsConfig.enrich || {}; + const enriched = { ...props }; + + // Add user context if enabled + if (enrichConfig.user !== false && this.currentUser?.user) { + enriched.user_id = this.currentUser.user.id; + } + + // Add organization context if enabled + if (enrichConfig.organization !== false && this.currentUser?.organization) { + enriched.organization_id = this.currentUser.organization.id; + } + + // Add timestamp if enabled + if (enrichConfig.timestamp !== false) { + enriched.timestamp = new Date().toISOString(); + } + + return enriched; + } + + /** + * Safely extracts the model name from a resource + * + * @private + * @param {Object} resource - The resource/model + * @returns {String} Model name or 'unknown' + */ + #getModelName(resource) { + if (!resource) { + return 'unknown'; + } + + // Try multiple ways to get model name + if (resource.constructor?.modelName) { + return resource.constructor.modelName; + } + + if (resource._internalModel?.modelName) { + return resource._internalModel.modelName; + } + + if (resource.modelName) { + return resource.modelName; + } + + return 'unknown'; + } +} diff --git a/addon/services/crud.js b/addon/services/crud.js index dca8c89b..53a8e24d 100644 --- a/addon/services/crud.js +++ b/addon/services/crud.js @@ -18,6 +18,7 @@ export default class CrudService extends Service { @service store; @service currentUser; @service universe; + @service analytics; /** * Generic deletion modal with options @@ -45,8 +46,8 @@ export default class CrudService extends Service { const response = await model.destroyRecord(); this.notifications.success(successNotification); - // Trigger analytics events - this._triggerResourceEvent('deleted', model); + // Track deletion event + this.analytics.trackResourceDeleted(model); if (typeof options.onSuccess === 'function') { options.onSuccess(model); @@ -167,11 +168,8 @@ export default class CrudService extends Service { this.notifications.success(response.message ?? successMessage); - // Trigger bulk action event - if (verb === 'delete') { - selected.forEach((model) => this._triggerResourceEvent('deleted', model)); - } - this.universe.trigger('resource.bulk_action', verb, selected, firstModel); + // Track bulk action event + this.analytics.trackBulkAction(verb, selected); if (typeof options.onSuccess === 'function') { options.onSuccess(selected); @@ -236,10 +234,8 @@ export default class CrudService extends Service { } ) .then(() => { - // Trigger export event - this.universe.trigger('resource.exported', modelName, format, exportParams); - const specificModelName = dasherize(modelName).replace(/-/g, '_'); - this.universe.trigger(`${specificModelName}.exported`, format, exportParams); + // Track export event + this.analytics.trackResourceExported(modelName, format, exportParams); later( this, @@ -265,24 +261,7 @@ export default class CrudService extends Service { * @param {Object} [options={}] * @memberof CrudService */ - /** - * Helper method to trigger both generic and specific resource events - * - * @private - * @param {String} action - The action performed (created, updated, deleted, etc.) - * @param {Model} model - The Ember Data model - */ - _triggerResourceEvent(action, model) { - // Trigger generic resource event - this.universe.trigger(`resource.${action}`, model); - - // Trigger specific model event (e.g., order.created, vehicle.updated) - const modelName = getModelName(model); - if (modelName) { - const specificModelName = dasherize(modelName).replace(/-/g, '_'); - this.universe.trigger(`${specificModelName}.${action}`, model); - } - } + @action import(modelName, options = {}) { // always lowercase modelname @@ -374,10 +353,9 @@ export default class CrudService extends Service { try { const response = await this.fetch.post(importEndpoint, { files }, fetchOptions); - // Trigger import event - this.universe.trigger('resource.imported', modelName, response, files); - const specificModelName = dasherize(modelName).replace(/-/g, '_'); - this.universe.trigger(`${specificModelName}.imported`, response, files); + // Track import event + const importCount = response?.imported?.length || response?.count || files.length; + this.analytics.trackResourceImported(modelName, importCount); if (typeof options.onImportCompleted === 'function') { options.onImportCompleted(response, files); diff --git a/addon/services/current-user.js b/addon/services/current-user.js index 8434877f..2db2930d 100644 --- a/addon/services/current-user.js +++ b/addon/services/current-user.js @@ -16,6 +16,7 @@ export default class CurrentUserService extends Service.extend(Evented) { @service theme; @service notifications; @service intl; + @service analytics; @tracked user = { id: 'anon' }; @tracked userSnapshot = { id: 'anon' }; @@ -71,6 +72,9 @@ export default class CurrentUserService extends Service.extend(Evented) { this.set('userSnapshot', snapshot); this.trigger('user.loaded', user); + // Track user loaded event + this.analytics.trackUserLoaded(user, user.company); + // Set permissions this.permissions = this.getUserPermissions(user); @@ -98,6 +102,9 @@ export default class CurrentUserService extends Service.extend(Evented) { this.set('userSnapshot', snapshot); this.trigger('user.loaded', user); + // Track user loaded event + this.analytics.trackUserLoaded(user, user.company); + // Set permissions this.permissions = this.getUserPermissions(user); diff --git a/addon/services/resource-action.js b/addon/services/resource-action.js index 62827c43..62999a55 100644 --- a/addon/services/resource-action.js +++ b/addon/services/resource-action.js @@ -30,6 +30,7 @@ export default class ResourceActionService extends Service { @service tableContext; @service resourceContextPanel; @service universe; + @service analytics; /** * Getter for router, attempt to use hostRouter if from engine @@ -300,8 +301,8 @@ export default class ResourceActionService extends Service { }) ); - // Trigger analytics events - this._triggerResourceEvent('created', record); + // Track creation event + this.analytics.trackResourceCreated(record); if (options.refresh) { this.refresh(); @@ -333,8 +334,8 @@ export default class ResourceActionService extends Service { }) ); - // Trigger analytics events - this._triggerResourceEvent('updated', record); + // Track update event + this.analytics.trackResourceUpdated(record); if (options.refresh) { this.refresh(); @@ -369,8 +370,12 @@ export default class ResourceActionService extends Service { }) ); - // Trigger analytics events - this._triggerResourceEvent(isNew ? 'created' : 'updated', record); + // Track save event (create or update) + if (isNew) { + this.analytics.trackResourceCreated(record); + } else { + this.analytics.trackResourceUpdated(record); + } if (options.refresh) { this.refresh(); @@ -419,8 +424,8 @@ export default class ResourceActionService extends Service { }) ); - // Trigger analytics events - this._triggerResourceEvent('deleted', record); + // Track deletion event + this.analytics.trackResourceDeleted(record); if (options.refresh) { this.refresh(); @@ -437,23 +442,7 @@ export default class ResourceActionService extends Service { } } - /** - * Helper method to trigger both generic and specific resource events - * - * @private - * @param {String} action - The action performed (created, updated, deleted, etc.) - * @param {Model} record - The Ember Data model - */ - _triggerResourceEvent(action, record) { - // Trigger generic resource event - this.universe.trigger(`resource.${action}`, record); - - // Trigger specific model event using the service's modelName - if (this.modelName) { - const specificModelName = this.modelName.replace(/-/g, '_'); - this.universe.trigger(`${specificModelName}.${action}`, record); - } - } + /** * Searches for records with debouncing. diff --git a/app/services/analytics.js b/app/services/analytics.js new file mode 100644 index 00000000..37069d48 --- /dev/null +++ b/app/services/analytics.js @@ -0,0 +1 @@ +export { default } from '@fleetbase/ember-core/services/analytics'; From 62ad87a1950339db2260330163d984e038c186e6 Mon Sep 17 00:00:00 2001 From: Manus AI Date: Tue, 10 Feb 2026 20:44:14 -0500 Subject: [PATCH 5/6] feat: Add dual event system to analytics service - Analytics service now extends Evented - Events fire on both analytics service and universe service - Allows local listeners (analytics.on) and cross-engine listeners (universe.on) - Updated documentation with dual event system examples - Follows Fleetbase patterns (like current-user service) Benefits: - Local listeners for UI updates and debugging - Cross-engine listeners for analytics integrations - More flexible event subscription options --- ANALYTICS_SERVICE.md | 68 ++++++++++++++++++++++++++++++++----- addon/services/analytics.js | 24 ++++++++----- 2 files changed, 75 insertions(+), 17 deletions(-) diff --git a/ANALYTICS_SERVICE.md b/ANALYTICS_SERVICE.md index 7025bc0c..30504d78 100644 --- a/ANALYTICS_SERVICE.md +++ b/ANALYTICS_SERVICE.md @@ -14,23 +14,31 @@ The analytics service is designed to: ## Architecture +The analytics service implements a **dual event system**, firing events on both the analytics service itself and the universe service: + ``` Service/Component ↓ analytics.trackResourceCreated(order) ↓ -Analytics Service +Analytics Service (extends Evented) - Enriches with metadata - Formats payload - ↓ -universe.trigger('resource.created', ...) -universe.trigger('order.created', ...) - ↓ -Internals (or other engines) - - Listens via universe.on() - - Translates to PostHog/etc + - Fires on TWO event buses: + ↓ ↓ +analytics.trigger() universe.trigger() + ↓ ↓ +Local Listeners Cross-Engine Listeners +(same app/engine) (internals, other engines) ``` +### Dual Event System Benefits + +1. **Local listeners** - Components can listen directly on `analytics.on()` +2. **Cross-engine listeners** - Engines listen on `universe.on()` +3. **Flexible** - Choose the right event bus for your use case +4. **Follows patterns** - Like `current-user` service which also extends `Evented` + ## Installation The analytics service is automatically available in all engines and the console application. It's exported globally via `host-services` and `services`. @@ -345,7 +353,11 @@ export default class OrderFormComponent extends Component { } ``` -### Listening to Events (in Engines) +### Listening to Events + +#### Option 1: Listen on Universe (Cross-Engine) + +Use this approach in engines (like `internals`) to listen to events from other parts of the application: ```javascript // In an engine's instance-initializer @@ -368,6 +380,44 @@ export function initialize(owner) { } ``` +#### Option 2: Listen on Analytics Service (Local) + +Use this approach for local listeners within the same app/engine: + +```javascript +import Component from '@glimmer/component'; +import { inject as service } from '@ember/service'; + +export default class DashboardComponent extends Component { + @service analytics; + + constructor() { + super(...arguments); + + // Listen to order creation events locally + this.analytics.on('order.created', (order, properties) => { + console.log('Order created:', order.id); + this.refreshDashboard(); + }); + } + + willDestroy() { + super.willDestroy(); + // Clean up listeners + this.analytics.off('order.created'); + } +} +``` + +#### Which Event Bus to Use? + +| Use Case | Event Bus | Example | +|----------|-----------|----------| +| Cross-engine tracking (PostHog, etc.) | `universe` | Internals listening to core events | +| Local UI updates | `analytics` | Dashboard refreshing on data changes | +| Debugging/logging | `analytics` | Development tools | +| Testing | `analytics` | Unit tests mocking events | + ## Best Practices 1. **Use specific tracking methods** - Prefer `trackResourceCreated()` over `trackEvent()` diff --git a/addon/services/analytics.js b/addon/services/analytics.js index d52172e9..48b33305 100644 --- a/addon/services/analytics.js +++ b/addon/services/analytics.js @@ -1,4 +1,5 @@ import Service, { inject as service } from '@ember/service'; +import Evented from '@ember/object/evented'; import { getOwner } from '@ember/application'; import config from 'ember-get-config'; @@ -12,7 +13,7 @@ import config from 'ember-get-config'; * @class AnalyticsService * @extends Service */ -export default class AnalyticsService extends Service { +export default class AnalyticsService extends Service.extend(Evented) { @service universe; @service currentUser; @@ -190,7 +191,11 @@ export default class AnalyticsService extends Service { // ========================================================================= /** - * Triggers an event on the universe service + * Triggers an event on both the analytics service and universe service + * + * This dual event system allows listeners to subscribe to events on either: + * - this.analytics.on('event.name', handler) - Local listeners + * - this.universe.on('event.name', handler) - Cross-engine listeners * * @private * @param {String} eventName - The event name @@ -201,17 +206,20 @@ export default class AnalyticsService extends Service { return; } - if (!this.universe) { - console.warn('[Analytics] Universe service not available'); - return; - } - // Debug logging if enabled if (config?.analytics?.debug) { console.log(`[Analytics] ${eventName}`, args); } - this.universe.trigger(eventName, ...args); + // Trigger on analytics service (local listeners) + this.trigger(eventName, ...args); + + // Trigger on universe service (cross-engine listeners) + if (this.universe) { + this.universe.trigger(eventName, ...args); + } else { + console.warn('[Analytics] Universe service not available'); + } } /** From c54b9cdc425a0807d0448c299f6b97407a768a5a Mon Sep 17 00:00:00 2001 From: Manus AI Date: Tue, 10 Feb 2026 20:55:34 -0500 Subject: [PATCH 6/6] refactor: Rename analytics service to events service - Renamed service from 'analytics' to 'events' for better clarity - Updated class name from AnalyticsService to EventsService - Updated all service injections (@service events) - Updated all method calls (this.events.*) - Updated configuration namespace (config.events) - Renamed documentation file to EVENTS_SERVICE.md - Updated global exports in host-services and services Rationale: - 'events' is more accurate - it's an event emitter - More generic and flexible - not limited to analytics use cases - Can be used for analytics, logging, UI updates, webhooks, etc. - Clearer purpose and intent --- ANALYTICS_SERVICE.md => EVENTS_SERVICE.md | 68 +++++++++++----------- addon/exports/host-services.js | 2 +- addon/exports/services.js | 2 +- addon/services/crud.js | 10 ++-- addon/services/current-user.js | 6 +- addon/services/{analytics.js => events.js} | 34 +++++------ addon/services/resource-action.js | 12 ++-- app/services/analytics.js | 1 - app/services/events.js | 1 + 9 files changed, 68 insertions(+), 68 deletions(-) rename ANALYTICS_SERVICE.md => EVENTS_SERVICE.md (85%) rename addon/services/{analytics.js => events.js} (90%) delete mode 100644 app/services/analytics.js create mode 100644 app/services/events.js diff --git a/ANALYTICS_SERVICE.md b/EVENTS_SERVICE.md similarity index 85% rename from ANALYTICS_SERVICE.md rename to EVENTS_SERVICE.md index 30504d78..32969e27 100644 --- a/ANALYTICS_SERVICE.md +++ b/EVENTS_SERVICE.md @@ -1,10 +1,10 @@ -# Analytics Service +# Events Service The `analytics` service provides a centralized, analytics-agnostic event tracking system for Fleetbase. It emits standardized events via the `universe` service's event bus, allowing engines (like `internals`) to subscribe and implement their own analytics integrations. ## Overview -The analytics service is designed to: +The events service is designed to: - **Centralize event emission** - Single source of truth for all analytics events - **Remain analytics-agnostic** - No vendor-specific code (PostHog, Google Analytics, etc.) @@ -14,19 +14,19 @@ The analytics service is designed to: ## Architecture -The analytics service implements a **dual event system**, firing events on both the analytics service itself and the universe service: +The events service implements a **dual event system**, firing events on both the events service itself and the universe service: ``` Service/Component ↓ -analytics.trackResourceCreated(order) +events.trackResourceCreated(order) ↓ -Analytics Service (extends Evented) +Events Service (extends Evented) - Enriches with metadata - Formats payload - Fires on TWO event buses: ↓ ↓ -analytics.trigger() universe.trigger() +events.trigger() universe.trigger() ↓ ↓ Local Listeners Cross-Engine Listeners (same app/engine) (internals, other engines) @@ -34,14 +34,14 @@ Local Listeners Cross-Engine Listeners ### Dual Event System Benefits -1. **Local listeners** - Components can listen directly on `analytics.on()` +1. **Local listeners** - Components can listen directly on `events.on()` 2. **Cross-engine listeners** - Engines listen on `universe.on()` 3. **Flexible** - Choose the right event bus for your use case 4. **Follows patterns** - Like `current-user` service which also extends `Evented` ## Installation -The analytics service is automatically available in all engines and the console application. It's exported globally via `host-services` and `services`. +The events service is automatically available in all engines and the console application. It's exported globally via `host-services` and `services`. ### Injection @@ -49,11 +49,11 @@ The analytics service is automatically available in all engines and the console import { inject as service } from '@ember/service'; export default class MyService extends Service { - @service analytics; + @service events; async createOrder(orderData) { const order = await this.store.createRecord('order', orderData).save(); - this.analytics.trackResourceCreated(order); + this.events.trackResourceCreated(order); return order; } } @@ -77,7 +77,7 @@ Tracks the creation of a new resource. **Example:** ```javascript -this.analytics.trackResourceCreated(order); +this.events.trackResourceCreated(order); // Emits: resource.created, order.created ``` @@ -95,7 +95,7 @@ Tracks the update of an existing resource. **Example:** ```javascript -this.analytics.trackResourceUpdated(driver); +this.events.trackResourceUpdated(driver); // Emits: resource.updated, driver.updated ``` @@ -113,7 +113,7 @@ Tracks the deletion of a resource. **Example:** ```javascript -this.analytics.trackResourceDeleted(vehicle); +this.events.trackResourceDeleted(vehicle); // Emits: resource.deleted, vehicle.deleted ``` @@ -131,7 +131,7 @@ Tracks a bulk import of resources. **Example:** ```javascript -this.analytics.trackResourceImported('contact', 50); +this.events.trackResourceImported('contact', 50); // Emits: resource.imported with count: 50 ``` @@ -151,7 +151,7 @@ Tracks a resource export. **Example:** ```javascript -this.analytics.trackResourceExported('order', 'csv', { status: 'completed' }); +this.events.trackResourceExported('order', 'csv', { status: 'completed' }); // Emits: resource.exported, order.exported ``` @@ -169,7 +169,7 @@ Tracks a bulk action on multiple resources. **Example:** ```javascript -this.analytics.trackBulkAction('delete', selectedOrders); +this.events.trackBulkAction('delete', selectedOrders); // Emits: resource.bulk_action with count and action ``` @@ -189,7 +189,7 @@ Tracks when the current user is loaded (session initialized). **Example:** ```javascript -this.analytics.trackUserLoaded(user, organization); +this.events.trackUserLoaded(user, organization); // Emits: user.loaded ``` @@ -206,7 +206,7 @@ Tracks when a user session ends. **Example:** ```javascript -this.analytics.trackSessionTerminated(3600); +this.events.trackSessionTerminated(3600); // Emits: session.terminated with duration ``` @@ -225,7 +225,7 @@ Tracks a generic custom event. **Example:** ```javascript -this.analytics.trackEvent('chat.message.sent', { length: 140 }); +this.events.trackEvent('chat.message.sent', { length: 140 }); // Emits: chat.message.sent ``` @@ -239,14 +239,14 @@ Checks if analytics tracking is enabled. **Example:** ```javascript -if (this.analytics.isEnabled()) { +if (this.events.isEnabled()) { // Tracking is enabled } ``` ## Configuration -The analytics service can be configured via `config/environment.js`: +The events service can be configured via `config/environment.js`: ```javascript // config/environment.js @@ -305,14 +305,14 @@ import Service, { inject as service } from '@ember/service'; export default class OrderService extends Service { @service store; - @service analytics; + @service events; async createOrder(orderData) { const order = this.store.createRecord('order', orderData); await order.save(); // Track the creation - this.analytics.trackResourceCreated(order); + this.events.trackResourceCreated(order); return order; } @@ -322,7 +322,7 @@ export default class OrderService extends Service { await order.save(); // Track the update - this.analytics.trackResourceUpdated(order); + this.events.trackResourceUpdated(order); return order; } @@ -337,7 +337,7 @@ import { inject as service } from '@ember/service'; import { action } from '@ember/object'; export default class OrderFormComponent extends Component { - @service analytics; + @service events; @action async saveOrder() { @@ -345,9 +345,9 @@ export default class OrderFormComponent extends Component { // Track the save if (this.args.order.isNew) { - this.analytics.trackResourceCreated(this.args.order); + this.events.trackResourceCreated(this.args.order); } else { - this.analytics.trackResourceUpdated(this.args.order); + this.events.trackResourceUpdated(this.args.order); } } } @@ -380,7 +380,7 @@ export function initialize(owner) { } ``` -#### Option 2: Listen on Analytics Service (Local) +#### Option 2: Listen on Events Service (Local) Use this approach for local listeners within the same app/engine: @@ -389,13 +389,13 @@ import Component from '@glimmer/component'; import { inject as service } from '@ember/service'; export default class DashboardComponent extends Component { - @service analytics; + @service events; constructor() { super(...arguments); // Listen to order creation events locally - this.analytics.on('order.created', (order, properties) => { + this.events.on('order.created', (order, properties) => { console.log('Order created:', order.id); this.refreshDashboard(); }); @@ -404,7 +404,7 @@ export default class DashboardComponent extends Component { willDestroy() { super.willDestroy(); // Clean up listeners - this.analytics.off('order.created'); + this.events.off('order.created'); } } ``` @@ -429,7 +429,7 @@ export default class DashboardComponent extends Component { ## Testing -To stub the analytics service in tests: +To stub the events service in tests: ```javascript import { module, test } from 'qunit'; @@ -466,10 +466,10 @@ this.universe.trigger('order.created', model); **After:** ```javascript -this.analytics.trackResourceCreated(model); +this.events.trackResourceCreated(model); // Automatically emits both events ``` ## Support -For questions or issues with the analytics service, please refer to the Fleetbase documentation or contact the development team. +For questions or issues with the events service, please refer to the Fleetbase documentation or contact the development team. diff --git a/addon/exports/host-services.js b/addon/exports/host-services.js index 91830230..f900dd2e 100644 --- a/addon/exports/host-services.js +++ b/addon/exports/host-services.js @@ -19,7 +19,7 @@ export const hostServices = [ 'sidebar', 'dashboard', 'universe', - 'analytics', + 'events', 'intl', 'abilities', 'language', diff --git a/addon/exports/services.js b/addon/exports/services.js index 79fa36c5..b51f3d21 100644 --- a/addon/exports/services.js +++ b/addon/exports/services.js @@ -21,7 +21,7 @@ export const services = [ 'sidebar', 'dashboard', 'universe', - 'analytics', + 'events', 'intl', 'abilities', 'language', diff --git a/addon/services/crud.js b/addon/services/crud.js index 53a8e24d..eb6d0bc3 100644 --- a/addon/services/crud.js +++ b/addon/services/crud.js @@ -18,7 +18,7 @@ export default class CrudService extends Service { @service store; @service currentUser; @service universe; - @service analytics; + @service events; /** * Generic deletion modal with options @@ -47,7 +47,7 @@ export default class CrudService extends Service { this.notifications.success(successNotification); // Track deletion event - this.analytics.trackResourceDeleted(model); + this.events.trackResourceDeleted(model); if (typeof options.onSuccess === 'function') { options.onSuccess(model); @@ -169,7 +169,7 @@ export default class CrudService extends Service { this.notifications.success(response.message ?? successMessage); // Track bulk action event - this.analytics.trackBulkAction(verb, selected); + this.events.trackBulkAction(verb, selected); if (typeof options.onSuccess === 'function') { options.onSuccess(selected); @@ -235,7 +235,7 @@ export default class CrudService extends Service { ) .then(() => { // Track export event - this.analytics.trackResourceExported(modelName, format, exportParams); + this.events.trackResourceExported(modelName, format, exportParams); later( this, @@ -355,7 +355,7 @@ export default class CrudService extends Service { // Track import event const importCount = response?.imported?.length || response?.count || files.length; - this.analytics.trackResourceImported(modelName, importCount); + this.events.trackResourceImported(modelName, importCount); if (typeof options.onImportCompleted === 'function') { options.onImportCompleted(response, files); diff --git a/addon/services/current-user.js b/addon/services/current-user.js index 2db2930d..f98a7d84 100644 --- a/addon/services/current-user.js +++ b/addon/services/current-user.js @@ -16,7 +16,7 @@ export default class CurrentUserService extends Service.extend(Evented) { @service theme; @service notifications; @service intl; - @service analytics; + @service events; @tracked user = { id: 'anon' }; @tracked userSnapshot = { id: 'anon' }; @@ -73,7 +73,7 @@ export default class CurrentUserService extends Service.extend(Evented) { this.trigger('user.loaded', user); // Track user loaded event - this.analytics.trackUserLoaded(user, user.company); + this.events.trackUserLoaded(user, user.company); // Set permissions this.permissions = this.getUserPermissions(user); @@ -103,7 +103,7 @@ export default class CurrentUserService extends Service.extend(Evented) { this.trigger('user.loaded', user); // Track user loaded event - this.analytics.trackUserLoaded(user, user.company); + this.events.trackUserLoaded(user, user.company); // Set permissions this.permissions = this.getUserPermissions(user); diff --git a/addon/services/analytics.js b/addon/services/events.js similarity index 90% rename from addon/services/analytics.js rename to addon/services/events.js index 48b33305..8c8cb017 100644 --- a/addon/services/analytics.js +++ b/addon/services/events.js @@ -4,16 +4,16 @@ import { getOwner } from '@ember/application'; import config from 'ember-get-config'; /** - * Analytics Service + * Events Service * - * Provides a centralized, analytics-agnostic event tracking system for Fleetbase. - * This service emits standardized events via the universe service's event bus, - * allowing engines (like internals) to subscribe and implement their own analytics. + * Provides a centralized event tracking system for Fleetbase. + * This service emits standardized events on both its own event bus and the universe service, + * allowing components, services, and engines to subscribe and react to application events. * - * @class AnalyticsService + * @class EventsService * @extends Service */ -export default class AnalyticsService extends Service.extend(Evented) { +export default class EventsService extends Service.extend(Evented) { @service universe; @service currentUser; @@ -177,13 +177,13 @@ export default class AnalyticsService extends Service.extend(Evented) { } /** - * Checks if analytics tracking is enabled + * Checks if event tracking is enabled * * @returns {Boolean} */ isEnabled() { - const analyticsConfig = config?.analytics || {}; - return analyticsConfig.enabled !== false; // Enabled by default + const eventsConfig = config?.events || {}; + return eventsConfig.enabled !== false; // Enabled by default } // ========================================================================= @@ -191,10 +191,10 @@ export default class AnalyticsService extends Service.extend(Evented) { // ========================================================================= /** - * Triggers an event on both the analytics service and universe service + * Triggers an event on both the events service and universe service * * This dual event system allows listeners to subscribe to events on either: - * - this.analytics.on('event.name', handler) - Local listeners + * - this.events.on('event.name', handler) - Local listeners * - this.universe.on('event.name', handler) - Cross-engine listeners * * @private @@ -207,18 +207,18 @@ export default class AnalyticsService extends Service.extend(Evented) { } // Debug logging if enabled - if (config?.analytics?.debug) { - console.log(`[Analytics] ${eventName}`, args); + if (config?.events?.debug) { + console.log(`[Events] ${eventName}`, args); } - // Trigger on analytics service (local listeners) + // Trigger on events service (local listeners) this.trigger(eventName, ...args); // Trigger on universe service (cross-engine listeners) if (this.universe) { this.universe.trigger(eventName, ...args); } else { - console.warn('[Analytics] Universe service not available'); + console.warn('[Events] Universe service not available'); } } @@ -274,8 +274,8 @@ export default class AnalyticsService extends Service.extend(Evented) { * @returns {Object} Enriched properties */ #enrichProperties(props = {}) { - const analyticsConfig = config?.analytics || {}; - const enrichConfig = analyticsConfig.enrich || {}; + const eventsConfig = config?.events || {}; + const enrichConfig = eventsConfig.enrich || {}; const enriched = { ...props }; // Add user context if enabled diff --git a/addon/services/resource-action.js b/addon/services/resource-action.js index 62999a55..7df7ac18 100644 --- a/addon/services/resource-action.js +++ b/addon/services/resource-action.js @@ -30,7 +30,7 @@ export default class ResourceActionService extends Service { @service tableContext; @service resourceContextPanel; @service universe; - @service analytics; + @service events; /** * Getter for router, attempt to use hostRouter if from engine @@ -302,7 +302,7 @@ export default class ResourceActionService extends Service { ); // Track creation event - this.analytics.trackResourceCreated(record); + this.events.trackResourceCreated(record); if (options.refresh) { this.refresh(); @@ -335,7 +335,7 @@ export default class ResourceActionService extends Service { ); // Track update event - this.analytics.trackResourceUpdated(record); + this.events.trackResourceUpdated(record); if (options.refresh) { this.refresh(); @@ -372,9 +372,9 @@ export default class ResourceActionService extends Service { // Track save event (create or update) if (isNew) { - this.analytics.trackResourceCreated(record); + this.events.trackResourceCreated(record); } else { - this.analytics.trackResourceUpdated(record); + this.events.trackResourceUpdated(record); } if (options.refresh) { @@ -425,7 +425,7 @@ export default class ResourceActionService extends Service { ); // Track deletion event - this.analytics.trackResourceDeleted(record); + this.events.trackResourceDeleted(record); if (options.refresh) { this.refresh(); diff --git a/app/services/analytics.js b/app/services/analytics.js deleted file mode 100644 index 37069d48..00000000 --- a/app/services/analytics.js +++ /dev/null @@ -1 +0,0 @@ -export { default } from '@fleetbase/ember-core/services/analytics'; diff --git a/app/services/events.js b/app/services/events.js new file mode 100644 index 00000000..c73e6c67 --- /dev/null +++ b/app/services/events.js @@ -0,0 +1 @@ +export { default } from '@fleetbase/ember-core/services/events';