Event Tracking Integration Guide for Backstage
This guide shows how to integrate the Fawkes event tracking library into Backstage components.
Setup
1. Initialize the Tracker
Add initialization to your Backstage app's entry point (e.g., packages/app/src/App.tsx or packages/app/src/index.tsx):
import { initializeTracker } from "@fawkes/design-system/analytics";
// Initialize tracker early in app lifecycle
initializeTracker({
baseUrl: "https://plausible.fawkes.idp",
domain: "backstage.fawkes.idp",
debug: process.env.NODE_ENV === "development",
trackLocalhost: true, // Enable for development
});
2. Update app-config.yaml
Ensure Plausible is configured in Backstage:
app:
analytics:
plausible:
domain: backstage.fawkes.idp
src: https://plausible.fawkes.idp/js/script.js
proxy:
endpoints:
"/plausible/api":
target: http://plausible.fawkes.svc:8000/api/
changeOrigin: true
secure: false
Usage Examples
Track Page Views
import { usePageViewTracking } from '@fawkes/design-system/analytics';
function HomePage() {
// Automatically track page view on mount
usePageViewTracking();
return <div>Welcome to Fawkes Platform</div>;
}
Track User Actions
import { useEventTracking, PredefinedEvents } from '@fawkes/design-system/analytics';
function CatalogPage() {
const { trackPredefined } = useEventTracking();
const handleViewService = (service: string) => {
trackPredefined(PredefinedEvents.VIEW_SERVICE, {
entityName: service,
entityKind: 'service',
});
};
return (
<ServiceList onServiceClick={handleViewService} />
);
}
Track Scaffolding Flow
import { useEventTracking, PredefinedEvents } from '@fawkes/design-system/analytics';
function ScaffoldingWizard() {
const { trackPredefined } = useEventTracking();
useEffect(() => {
// Track scaffolding start
trackPredefined(PredefinedEvents.START_SCAFFOLDING);
}, []);
const handleTemplateSelection = (template: string) => {
trackPredefined(PredefinedEvents.SELECT_TEMPLATE, {
template,
language: getLanguageFromTemplate(template),
});
};
const handleComplete = (serviceName: string) => {
trackPredefined(PredefinedEvents.COMPLETE_SCAFFOLDING, {
entityName: serviceName,
template: selectedTemplate,
});
};
const handleCancel = () => {
trackPredefined(PredefinedEvents.CANCEL_SCAFFOLDING);
};
return (
<Wizard
onTemplateSelect={handleTemplateSelection}
onComplete={handleComplete}
onCancel={handleCancel}
/>
);
}
Track Search Queries
import { useSearchTracking } from '@fawkes/design-system/analytics';
function CatalogSearch() {
const { trackSearch } = useSearchTracking('catalog');
const handleSearch = (query: string, results: any[]) => {
trackSearch(query, results.length);
};
return (
<SearchBar
onSearch={(query) => {
const results = performSearch(query);
handleSearch(query, results);
return results;
}}
/>
);
}
Track CI/CD Events
import { useEventTracking, PredefinedEvents } from '@fawkes/design-system/analytics';
function PipelineCard({ pipelineId }: { pipelineId: string }) {
const { trackPredefined } = useEventTracking();
const handleTriggerBuild = async () => {
const startTime = Date.now();
trackPredefined(PredefinedEvents.TRIGGER_BUILD, {
pipelineId,
component: 'PipelineCard',
});
try {
await triggerJenkinsBuild(pipelineId);
trackPredefined(PredefinedEvents.BUILD_COMPLETE, {
pipelineId,
duration: Date.now() - startTime,
});
} catch (error) {
trackPredefined(PredefinedEvents.BUILD_FAILED, {
pipelineId,
errorCode: error.code,
errorMessage: error.message,
});
}
};
const handleViewLogs = () => {
trackPredefined(PredefinedEvents.VIEW_BUILD_LOGS, {
pipelineId,
});
};
return (
<Card>
<Button onClick={handleTriggerBuild}>Trigger Build</Button>
<Button onClick={handleViewLogs}>View Logs</Button>
</Card>
);
}
Track Deployment Events
import { useEventTracking, PredefinedEvents } from '@fawkes/design-system/analytics';
function DeployButton({ serviceName, environment }: DeployButtonProps) {
const { trackPredefined } = useEventTracking();
const handleDeploy = async () => {
trackPredefined(PredefinedEvents.DEPLOY_APPLICATION, {
entityName: serviceName,
deploymentTarget: environment,
});
try {
await deployToArgoCD(serviceName, environment);
trackPredefined(PredefinedEvents.DEPLOYMENT_COMPLETE, {
entityName: serviceName,
deploymentTarget: environment,
});
} catch (error) {
trackPredefined(PredefinedEvents.DEPLOYMENT_FAILED, {
entityName: serviceName,
deploymentTarget: environment,
errorMessage: error.message,
});
}
};
return (
<Button onClick={handleDeploy}>
Deploy to {environment}
</Button>
);
}
Track Form Interactions
import { useFormTracking } from '@fawkes/design-system/analytics';
function ServiceCreationForm() {
const { trackFormStart, trackFormSubmit, trackFormError } = useFormTracking('service-creation');
useEffect(() => {
trackFormStart();
}, []);
const handleSubmit = async (data: FormData) => {
try {
await createService(data);
trackFormSubmit({
template: data.template,
language: data.language,
});
} catch (error) {
trackFormError(error.message, {
field: error.field,
});
}
};
return <Form onSubmit={handleSubmit}>...</Form>;
}
Track Errors
import { useErrorTracking } from '@fawkes/design-system/analytics';
function ServiceDetailPage({ serviceId }: { serviceId: string }) {
const { trackError, trackAPIError } = useErrorTracking();
const fetchServiceData = async () => {
try {
const response = await fetch(`/api/services/${serviceId}`);
if (!response.ok) {
trackAPIError(
`/api/services/${serviceId}`,
response.status,
response.statusText
);
throw new Error('Failed to fetch service data');
}
return response.json();
} catch (error) {
trackError(error, {
component: 'ServiceDetailPage',
serviceId,
});
throw error;
}
};
return <ErrorBoundary onError={trackError}>...</ErrorBoundary>;
}
Track Performance
import { usePerformanceTracking } from '@fawkes/design-system/analytics';
function HeavyComponent() {
const { trackPerformance } = usePerformanceTracking('heavy-component-load');
useEffect(() => {
const startTime = performance.now();
loadHeavyData().then(() => {
const duration = performance.now() - startTime;
trackPerformance(duration, {
component: 'HeavyComponent',
});
});
}, []);
return <div>...</div>;
}
Track Button Clicks
import { useButtonClick } from '@fawkes/design-system/analytics';
function FeedbackWidget() {
const trackOpenFeedback = useButtonClick('Open Feedback', {
component: 'FeedbackWidget',
});
return (
<IconButton onClick={trackOpenFeedback}>
<FeedbackIcon />
</IconButton>
);
}
Track Documentation Access
import { useEventTracking, PredefinedEvents } from '@fawkes/design-system/analytics';
function TechDocsViewer({ docId }: { docId: string }) {
const { trackPredefined } = useEventTracking();
useEffect(() => {
trackPredefined(PredefinedEvents.VIEW_TECHDOCS, {
entityId: docId,
});
}, [docId]);
const handleDownload = () => {
trackPredefined(PredefinedEvents.DOWNLOAD_DOCS, {
entityId: docId,
format: 'pdf',
});
};
return (
<div>
<DocContent docId={docId} />
<Button onClick={handleDownload}>Download PDF</Button>
</div>
);
}
Track Plugin Interactions
import { useEventTracking, PredefinedEvents } from '@fawkes/design-system/analytics';
function KubernetesPlugin() {
const { trackPredefined } = useEventTracking();
useEffect(() => {
trackPredefined(PredefinedEvents.USE_KUBERNETES_PLUGIN);
}, []);
return <KubernetesContent />;
}
function ArgoCDPlugin() {
const { trackPredefined } = useEventTracking();
useEffect(() => {
trackPredefined(PredefinedEvents.USE_ARGOCD_PLUGIN);
}, []);
return <ArgoCDContent />;
}
Track Component Lifecycle
import { useComponentTracking } from '@fawkes/design-system/analytics';
function ServiceCard({ service }: { service: Service }) {
useComponentTracking('ServiceCard', {
entityName: service.name,
entityKind: 'service',
});
return <Card>...</Card>;
}
Track Navigation
import { useNavigationTracking } from '@fawkes/design-system/analytics';
import { useNavigate } from 'react-router-dom';
function NavigationMenu() {
const { trackNavigation } = useNavigationTracking();
const navigate = useNavigate();
const handleNavigate = (path: string) => {
trackNavigation(path, {
source: 'navigation-menu',
});
navigate(path);
};
return (
<Menu>
<MenuItem onClick={() => handleNavigate('/catalog')}>
Catalog
</MenuItem>
<MenuItem onClick={() => handleNavigate('/docs')}>
Documentation
</MenuItem>
</Menu>
);
}
Advanced Usage
Custom Middleware
import {
initializeTracker,
EventTracker,
MiddlewareChain,
validationMiddleware,
privacyMiddleware,
enrichmentMiddleware,
userContextMiddleware,
} from "@fawkes/design-system/analytics";
// Create custom middleware
const customMiddleware = (event) => {
// Add custom logic
return {
...event,
properties: {
...event.properties,
appVersion: "1.0.0",
},
};
};
// Configure middleware chain
const chain = new MiddlewareChain();
chain.use(validationMiddleware);
chain.use(privacyMiddleware);
chain.use(enrichmentMiddleware({ platform: "fawkes" }));
chain.use(
userContextMiddleware(() => ({
userId: getCurrentUser()?.id,
team: getCurrentUser()?.team,
}))
);
chain.use(customMiddleware);
// Initialize tracker
const tracker = initializeTracker({
baseUrl: "https://plausible.fawkes.idp",
domain: "backstage.fawkes.idp",
});
// Track with middleware
const processedEvent = chain.execute(event);
if (processedEvent) {
tracker.track(processedEvent);
}
Custom Events
import { trackEvent, EventCategory, EventAction } from "@fawkes/design-system/analytics";
// Track custom event
trackEvent({
category: EventCategory.FEATURE_USAGE,
action: EventAction.CLICK,
label: "Custom Feature",
properties: {
featureName: "my-custom-feature",
version: "2.0",
},
value: 1,
});
Conditional Tracking
import { getTracker } from '@fawkes/design-system/analytics';
function ConditionalTracking() {
const tracker = getTracker();
// Enable/disable tracking based on user preference
const handleToggleTracking = (enabled: boolean) => {
tracker.setEnabled(enabled);
};
// Check tracker status
const status = tracker.getStatus();
console.log('Tracker initialized:', status.initialized);
console.log('Queued events:', status.queueSize);
return (
<Toggle
checked={status.initialized}
onChange={handleToggleTracking}
label="Enable Analytics"
/>
);
}
Best Practices
- Track User Intent: Focus on what the user is trying to accomplish, not technical implementation details
- Use Predefined Events: Prefer predefined events for consistency
- Add Context: Include relevant properties for better insights
- Handle Errors: Always track errors with context
- Performance: Track performance metrics for slow operations
- Privacy: Never include PII in event properties
- Validation: Events are automatically validated before sending
Testing
import { trackEvent } from '@fawkes/design-system/analytics';
// Mock in tests
jest.mock('@fawkes/design-system/analytics', () => ({
trackEvent: jest.fn(),
trackPredefinedEvent: jest.fn(),
useEventTracking: () => ({
track: jest.fn(),
trackPredefined: jest.fn(),
}),
}));
// Test tracking
test('tracks deployment event', () => {
const { getByText } = render(<DeployButton />);
fireEvent.click(getByText('Deploy'));
expect(trackEvent).toHaveBeenCalledWith(
expect.objectContaining({
category: EventCategory.DEPLOYMENT,
action: EventAction.DEPLOY,
})
);
});
Viewing Analytics
- Navigate to
https://plausible.fawkes.idp - Login with admin credentials
- Select
backstage.fawkes.idpsite - View custom events in Goals section
- Create dashboards and reports
Troubleshooting
Events Not Appearing
- Check tracker initialization:
getTracker().getStatus() - Verify Plausible script loaded in browser DevTools
- Enable debug mode:
debug: truein config - Check browser console for errors
Missing Events
- Verify event validation passed
- Check middleware isn't filtering events
- Ensure Plausible service is running
- Review event naming and properties
For more details, see the Analytics README.