Add billing usage analytics dashboard with ClickHouse integration (#18592)
## Summary This PR adds a comprehensive billing usage analytics feature that provides detailed breakdowns of credit consumption across execution types, users, resources, and time periods. The implementation includes a new ClickHouse-backed analytics service, GraphQL API endpoint, and a frontend dashboard component. ## Key Changes ### Backend - **New BillingAnalyticsService**: Queries ClickHouse for usage breakdowns by user, resource, execution type, and time series data - **BillingEventWriterService**: Writes billing events to ClickHouse for analytics while maintaining best-effort semantics (never blocks Stripe billing) - **ClickHouse Schema**: Added `billingEvent` table with 3-year TTL for storing detailed billing event data - **GraphQL Resolver**: New `getBillingAnalytics` query that aggregates usage data for the current billing period, protected by feature flag and billing permissions - **Enhanced BillingUsageEvent**: Added `userWorkspaceId` field to track per-user credit consumption - **AI Billing Integration**: Updated AI billing service to pass `userWorkspaceId` when recording usage events ### Frontend - **SettingsBillingAnalyticsSection**: New component displaying: - Usage breakdown by execution type with progress bars - Daily usage time series chart (28-day view) - Per-user credit consumption breakdown - Per-resource (agent/workflow) credit consumption breakdown - **SettingsUsage Page**: Dedicated page for viewing usage analytics - **GraphQL Query**: `GetBillingAnalytics` query with generated hooks - **Navigation**: Added Usage menu item in settings (feature-flagged) - **Mock Data**: Included screenshot mock data for preview/testing ### Feature Flag - Added `IS_USAGE_ANALYTICS_ENABLED` feature flag to control visibility and access to analytics features ## Implementation Details - Analytics data is queried in parallel for performance - ClickHouse writes are non-blocking to ensure billing operations never fail - Progress bars use dynamic coloring from a predefined palette - Time series visualization normalizes bar heights relative to max value - Empty state handling when no analytics data is available - Responsive UI with proper text truncation for long names https://claude.ai/code/session_01Y1EqrX6PFq3EJxJq89h7DF --------- Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
+6
@@ -73,6 +73,11 @@ export class AiAgentWorkflowAction implements WorkflowAction {
|
||||
const executionContext =
|
||||
await this.workflowExecutionContextService.getExecutionContext(runInfo);
|
||||
|
||||
const userWorkspaceId =
|
||||
executionContext.authContext.type === 'user'
|
||||
? executionContext.authContext.userWorkspaceId
|
||||
: null;
|
||||
|
||||
const { result, usage, cacheCreationTokens } =
|
||||
await this.aiAgentExecutionService.executeAgent({
|
||||
agent,
|
||||
@@ -89,6 +94,7 @@ export class AiAgentWorkflowAction implements WorkflowAction {
|
||||
{ usage, cacheCreationTokens },
|
||||
workspaceId,
|
||||
agent?.id || null,
|
||||
userWorkspaceId,
|
||||
);
|
||||
|
||||
return {
|
||||
|
||||
+18
-18
@@ -2,9 +2,11 @@ import { Test, type TestingModule } from '@nestjs/testing';
|
||||
|
||||
import { getWorkflowRunContext, StepStatus } from 'twenty-shared/workflow';
|
||||
|
||||
import { BILLING_FEATURE_USED } from 'src/engine/core-modules/billing/constants/billing-feature-used.constant';
|
||||
import { BILLING_WORKFLOW_EXECUTION_ERROR_MESSAGE } from 'src/engine/core-modules/billing/constants/billing-workflow-execution-error-message.constant';
|
||||
import { BillingMeterEventName } from 'src/engine/core-modules/billing/enums/billing-meter-event-names';
|
||||
import { USAGE_RECORDED } from 'src/engine/core-modules/usage/constants/usage-recorded.constant';
|
||||
import { UsageOperationType } from 'src/engine/core-modules/usage/enums/usage-operation-type.enum';
|
||||
import { UsageResourceType } from 'src/engine/core-modules/usage/enums/usage-resource-type.enum';
|
||||
import { UsageUnit } from 'src/engine/core-modules/usage/enums/usage-unit.enum';
|
||||
import { BillingService } from 'src/engine/core-modules/billing/services/billing.service';
|
||||
import { ExceptionHandlerService } from 'src/engine/core-modules/exception-handler/exception-handler.service';
|
||||
import { MessageQueue } from 'src/engine/core-modules/message-queue/message-queue.constants';
|
||||
@@ -190,16 +192,15 @@ describe('WorkflowExecutorWorkspaceService', () => {
|
||||
});
|
||||
|
||||
expect(workspaceEventEmitter.emitCustomBatchEvent).toHaveBeenCalledWith(
|
||||
BILLING_FEATURE_USED,
|
||||
USAGE_RECORDED,
|
||||
[
|
||||
{
|
||||
eventName: BillingMeterEventName.WORKFLOW_NODE_RUN,
|
||||
value: 1,
|
||||
dimensions: {
|
||||
execution_type: 'workflow_execution',
|
||||
resource_id: 'workflow-id',
|
||||
execution_context_1: null,
|
||||
},
|
||||
resourceType: UsageResourceType.WORKFLOW,
|
||||
operationType: UsageOperationType.WORKFLOW_EXECUTION,
|
||||
creditsUsedMicro: 1,
|
||||
quantity: 1,
|
||||
unit: UsageUnit.INVOCATION,
|
||||
resourceId: 'workflow-id',
|
||||
},
|
||||
],
|
||||
'workspace-id',
|
||||
@@ -648,16 +649,15 @@ describe('WorkflowExecutorWorkspaceService', () => {
|
||||
service['sendWorkflowNodeRunEvent']('workspace-id', 'workflow-id');
|
||||
|
||||
expect(workspaceEventEmitter.emitCustomBatchEvent).toHaveBeenCalledWith(
|
||||
BILLING_FEATURE_USED,
|
||||
USAGE_RECORDED,
|
||||
[
|
||||
{
|
||||
eventName: BillingMeterEventName.WORKFLOW_NODE_RUN,
|
||||
value: 1,
|
||||
dimensions: {
|
||||
execution_type: 'workflow_execution',
|
||||
resource_id: 'workflow-id',
|
||||
execution_context_1: null,
|
||||
},
|
||||
resourceType: UsageResourceType.WORKFLOW,
|
||||
operationType: UsageOperationType.WORKFLOW_EXECUTION,
|
||||
creditsUsedMicro: 1,
|
||||
quantity: 1,
|
||||
unit: UsageUnit.INVOCATION,
|
||||
resourceId: 'workflow-id',
|
||||
},
|
||||
],
|
||||
'workspace-id',
|
||||
|
||||
+13
-12
@@ -9,12 +9,14 @@ import {
|
||||
WorkflowRunStepInfos,
|
||||
} from 'twenty-shared/workflow';
|
||||
|
||||
import { BILLING_FEATURE_USED } from 'src/engine/core-modules/billing/constants/billing-feature-used.constant';
|
||||
import { BILLING_WORKFLOW_EXECUTION_ERROR_MESSAGE } from 'src/engine/core-modules/billing/constants/billing-workflow-execution-error-message.constant';
|
||||
import { BillingMeterEventName } from 'src/engine/core-modules/billing/enums/billing-meter-event-names';
|
||||
import { USAGE_RECORDED } from 'src/engine/core-modules/usage/constants/usage-recorded.constant';
|
||||
import { BillingProductKey } from 'src/engine/core-modules/billing/enums/billing-product-key.enum';
|
||||
import { UsageOperationType } from 'src/engine/core-modules/usage/enums/usage-operation-type.enum';
|
||||
import { UsageResourceType } from 'src/engine/core-modules/usage/enums/usage-resource-type.enum';
|
||||
import { UsageUnit } from 'src/engine/core-modules/usage/enums/usage-unit.enum';
|
||||
import { BillingService } from 'src/engine/core-modules/billing/services/billing.service';
|
||||
import { type BillingUsageEvent } from 'src/engine/core-modules/billing/types/billing-usage-event.type';
|
||||
import { type UsageEvent } from 'src/engine/core-modules/usage/types/usage-event.type';
|
||||
import { ExceptionHandlerService } from 'src/engine/core-modules/exception-handler/exception-handler.service';
|
||||
import { InjectMessageQueue } from 'src/engine/core-modules/message-queue/decorators/message-queue.decorator';
|
||||
import { MessageQueue } from 'src/engine/core-modules/message-queue/message-queue.constants';
|
||||
@@ -353,17 +355,16 @@ export class WorkflowExecutorWorkspaceService {
|
||||
}
|
||||
|
||||
private sendWorkflowNodeRunEvent(workspaceId: string, workflowId: string) {
|
||||
this.workspaceEventEmitter.emitCustomBatchEvent<BillingUsageEvent>(
|
||||
BILLING_FEATURE_USED,
|
||||
this.workspaceEventEmitter.emitCustomBatchEvent<UsageEvent>(
|
||||
USAGE_RECORDED,
|
||||
[
|
||||
{
|
||||
eventName: BillingMeterEventName.WORKFLOW_NODE_RUN,
|
||||
value: 1,
|
||||
dimensions: {
|
||||
execution_type: 'workflow_execution',
|
||||
resource_id: workflowId,
|
||||
execution_context_1: null,
|
||||
},
|
||||
resourceType: UsageResourceType.WORKFLOW,
|
||||
operationType: UsageOperationType.WORKFLOW_EXECUTION,
|
||||
creditsUsedMicro: 1,
|
||||
quantity: 1,
|
||||
unit: UsageUnit.INVOCATION,
|
||||
resourceId: workflowId,
|
||||
},
|
||||
],
|
||||
workspaceId,
|
||||
|
||||
Reference in New Issue
Block a user