Experimental Feature • This guide explains how to use the User Actions API to manually control user action creation, monitoring, and lifecycle management in your web application.
The User Actions API provides fine-grained control over user action creation and completion. You can create custom user actions, disable automatic detection, and subscribe to user action events. This is particularly useful when the automatic user action detection interferes with your application's behavior or when you need precise control over action boundaries.
📝 Note
dynatrace.userActions property is undefined if the User Actions module is disabled.
With the User Actions API, you can:
Before using the User Actions API, ensure that:
dynatrace global objectRUM JavaScript detects user interactions and processes that follow them. If a process follows a user interaction, Dynatrace creates a user action for it. This user action lasts as long as there's activity on the page, like DOM mutations or requests.
You can manipulate the behavior using the API described on this page.
There can always only be one user action active at any point in time. Whenever a user action is active and a new one is about to start, the active user action completes and the new one starts. The user action event provides information about how the completion happened:
User actions can complete for various reasons, indicated by the user_action.complete_reason property:
completed - The user action completed normally after the inactivity threshold was reached. This occurs when Dynatrace detects no more activity (DOM mutations, requests, etc.) related to the user interaction.
completed_by_api - The user action was completed explicitly by calling the finish() function through the API.
interrupted_by_api - The user action was interrupted when a new user action was created via the API using create().
interrupted_by_navigation - The user action was interrupted by a page navigation event, such as clicking a link or using browser navigation buttons.
interrupted_by_request - The user action was interrupted when a new user interaction followed by an XHR or fetch request triggered the start of a new user action.
no_activity - The user action completed because there was no activity detected on the page. This only affects navigation user actions.
page_hide - The user action completed because the page was dismissed (user navigated away, closed the tab, or the page hide event was fired).
timeout - The user action exceeded the maximum allowed duration and was forcefully completed to prevent indefinitely running actions.
You can use the User Actions API directly, or via its synchronous or asynchronous SDK functions, matching the pattern established in the RUM JavaScript SDK Overview:
dynatrace.userActions)Access the API directly through the global dynatrace.userActions namespace:
const userAction = dynatrace?.userActions?.create();
userAction?.finish();
This approach requires optional chaining (?.) to handle cases where RUM JavaScript or the module is not loaded.
Use the safe wrapper functions from @dynatrace/rum-javascript-sdk/api that gracefully handle missing modules:
import { create } from '@dynatrace/rum-javascript-sdk/api/user-actions';
const userAction = create({ autoClose: false });
// No need for optional chaining - returns undefined if module is unavailable
userAction?.finish();
Use the promise-based API from @dynatrace/rum-javascript-sdk/api/promises when you need to ensure the module is available:
import { create } from '@dynatrace/rum-javascript-sdk/api/promises/user-actions';
try {
const userAction = await create({ autoClose: false });
// Guaranteed to have a user action or will throw
userAction.finish();
} catch (error) {
console.error('User Actions module not available:', error);
}
💡 Tip
You can create custom user actions to track specific interactions or workflows in your application. Custom user actions allow you to measure timing and associate related events.
Create a simple user action and finish it manually:
// Create a new user action with default settings
const userAction = dynatrace.userActions?.create();
// Perform your application logic here
performSomeWork();
// Manually finish the user action
userAction?.finish();
By default, user actions are automatically closed when Dynatrace detects that the action is complete according to the RUM JavaScript user action rules.
🚨 Caution
finish() is reliable in this case because performSomeWork() is synchronous. If it was awaited, there is no
guarantee that the action would still be open when finish() is called, leading to unreliable action durations. See
(User action with manual control)[#user-action-with-manual-control] below.
Disable automatic closing to maintain full control over when the user action completes:
// Create a user action that won't auto-close
const userAction = dynatrace.userActions?.create({ autoClose: false });
// Execute async operations
await fetchData();
await processResults();
// Finish the user action when ready
userAction?.finish();
⚠️ Warning
autoClose is set to false, you must call finish() manually. Otherwise, the user action will remain open until the next user action is created or the maximum duration is reached, which may lead to incorrect timing measurements.
Subscribe to user action completion events to understand when actions would have been completed automatically:
const userAction = dynatrace.userActions?.create({ autoClose: false });
// Subscribe to completion events
const unsubscribe = userAction?.subscribe(currentUserAction => {
// This is just an informational log - we intentionally ignore automatic closing. We finish the user action manually later on.
console.log(`User action would have been completed automatically`);
});
// Perform your operations
await router.navigate('/home');
// Finish the user action
userAction?.finish();
// Clean up the subscription when done
unsubscribe?.();
Set a custom name for your user action to make it more identifiable in Dynatrace:
const userAction = dynatrace.userActions?.create({ autoClose: false });
// Set a descriptive name
if (userAction) {
userAction.name = 'Checkout Flow - Payment Processing';
}
// Perform payment processing
await processPayment();
userAction?.finish();
Attach custom properties to user actions to provide additional context:
const userAction = dynatrace.userActions?.current;
if (userAction) {
// Set custom properties
userAction.event_properties = {
'event_properties.transaction_id': 'TXN-123456',
'event_properties.payment_method': 'credit_card',
'event_properties.amount': 99.99,
'event_properties.currency': 'USD',
'event_properties.is_first_purchase': true
};
}
Or use the addEventModifier API to attach them to events directly:
dynatrace.addEventModifier(evt => {
if (evt.has_user_action) {
return {
...evt,
'event_properties.transaction_id': 'TXN-123456',
'event_properties.payment_method': 'credit_card',
'event_properties.amount': 99.99,
'event_properties.currency': 'USD',
'event_properties.is_first_purchase': true
}
}
return evt;
})
📝 Note
event_properties requires you to configure them on the server side.
See Event and session properties for more details.
Monitor the state of a user action to determine if it's still active:
const userAction = dynatrace.userActions?.create();
// Perform some operations
await fetchData();
// Check if the user action is still active
if (userAction?.state === 'active') {
console.log('User action is still running');
// Continue with more operations
await processData();
} else {
console.log('User action has already completed');
}
Monitor all user actions created by Dynatrace to intercept and modify their behavior. This is useful for implementing global user action policies across your application.
Subscribe to user action creation events:
const unsubscribe = dynatrace.userActions?.subscribe(userAction => {
console.log('New user action created:', userAction);
});
// Later, when you no longer need to monitor user actions
unsubscribe?.();
Change how user actions behave by modifying their properties:
dynatrace.userActions?.subscribe(userAction => {
// Disable automatic completion for all user actions
userAction.autoClose = false;
// Perform custom logic
performCustomTracking(userAction);
// Manually control completion
setTimeout(() => {
userAction.finish();
}, 5000);
});
Apply different behavior based on the user action context:
dynatrace.userActions?.subscribe(userAction => {
// Only modify actions with name Add to Cart
if (userAction.name === 'Add to Cart') {
userAction.autoClose = false;
// Add custom completion logic
whenCustomConditionMet().then(() => {
userAction.finish();
});
}
});
// some time later
dynatrace.userActions?.create({ name: "Add to Cart"});
Disable automatic user action detection when it interferes with manual user action handling. This is particularly useful for single-page applications with complex routing logic.
// Disable automatic user action detection
dynatrace.userActions?.setAutomaticDetection(false);
// Now you have full control over user action creation
async function handleNavigation(route) {
const userAction = dynatrace.userActions?.create({ autoClose: false });
// Navigate without triggering automatic user action
await router.navigate(route);
// Manually finish when ready
userAction?.finish();
}
🚨 Caution
create() function.
// Re-enable automatic user action detection
dynatrace.userActions?.setAutomaticDetection(true);
The current property provides access to the currently active user action. This is useful when you need to modify an ongoing action or prevent its automatic completion.
async function postMessage(message, channel) {
const currentUserAction = dynatrace.userActions?.current;
if (currentUserAction) {
// Prevent automatic completion
currentUserAction.autoClose = false;
}
// Perform async operation
const response = await channel.send(message);
// Manually finish the user action
currentUserAction?.finish();
return response;
}
Prevent automatic user action completion when handling click events that trigger complex navigation:
dynatrace.userActions?.setAutomaticDetection(false);
document.addEventListener('click', async (event) => {
const target = event.target as HTMLElement;
if (target.matches('[data-navigate]')) {
const userAction = dynatrace.userActions?.create({ autoClose: false });
const route = target.getAttribute('data-navigate');
// Set custom name and properties
if (userAction) {
userAction.name = `Navigate to ${route}`;
userAction.event_properties = {
'event_properties.target_route': route,
'event_properties.navigation_trigger': 'click'
};
}
// This would normally create an automatic user action
await router.redirect(route);
// Wait for the route to fully load
await waitForRouteReady();
// Now finish the user action
userAction?.finish();
}
});
Create a user action that spans multiple asynchronous operations:
async function handleComplexWorkflow(workflowId: string, priority: string) {
const startTime = Date.now();
const userAction = dynatrace.userActions?.create({ autoClose: false });
if (userAction) {
// Set up user action metadata
userAction.name = 'Complex Data Workflow';
userAction.startTime = startTime;
userAction.event_properties = {
'event_properties.workflow_id': workflowId,
'event_properties.priority': priority,
'event_properties.step_count': 3
};
}
try {
// Step 1: Fetch user data
const userData = await fetchUserData();
// Update properties after step 1
if (userAction && userAction.state === 'active') {
userAction.event_properties = {
...userAction.event_properties,
'event_properties.current_step': 1,
'event_properties.records_fetched': userData.length
};
}
// Step 2: Process data
const processedData = await processData(userData);
// Update properties after step 2
if (userAction && userAction.state === 'active') {
userAction.event_properties = {
...userAction.event_properties,
'event_properties.current_step': 2,
'event_properties.records_processed': processedData.length
};
}
// Step 3: Submit results
const result = await submitResults(processedData);
// Final update
if (userAction && userAction.state === 'active') {
userAction.event_properties = {
...userAction.event_properties,
'event_properties.current_step': 3,
'event_properties.workflow_status': 'completed',
'event_properties.result_id': result.id
};
}
} catch (error) {
console.error('Workflow failed:', error);
// Mark the workflow as failed in properties
if (userAction && userAction.state === 'active') {
userAction.event_properties = {
...userAction.event_properties,
'event_properties.workflow_status': 'failed',
'event_properties.error_message': error.message
};
}
} finally {
// make sure to complete the user action in any case
userAction?.finish();
}
}
Modify the start time of a user action for more accurate measurements:
// Record the actual start time before any setup
const actualStartTime = Date.now();
// Do some setup work that shouldn't be part of the measurement
await loadConfiguration();
await initializeServices();
// Create the user action and set the start time
const userAction = dynatrace.userActions?.create({ autoClose: false });
if (userAction) {
// Set the start time to when the actual work began
userAction.startTime = actualStartTime;
userAction.name = 'Data Processing Operation';
}
// Perform the actual measured work
await processData();
userAction?.finish();
Integrate with popular frameworks like React Router:
// Example with React Router
import { useEffect } from 'react';
import { useNavigate, useLocation } from 'react-router-dom';
function useManualUserActionTracking() {
const navigate = useNavigate();
const location = useLocation();
useEffect(() => {
// Disable automatic detection to prevent conflicts
dynatrace.userActions?.setAutomaticDetection(false);
}, []);
const trackedNavigate = async (path: string) => {
// Don't use autoClose here to allow Dynatrace to determine when the navigation is complete
const userAction = dynatrace.userActions?.create();
// Add metadata if provided
if (userAction) {
userAction.name = `Route: ${path}`;
userAction.event_properties = {
'event_properties.route': path,
'event_properties.previous_route': location.pathname
};
}
// Perform navigation
navigate(path);
};
return trackedNavigate;
}
A complete example tracking an e-commerce checkout with detailed metadata:
class CheckoutTracker {
private checkoutAction?: UserActionTracker;
startCheckout(cart: Cart) {
// Record the actual start time
const checkoutStartTime = Date.now();
this.checkoutAction = dynatrace.userActions?.create({ autoClose: false });
if (this.checkoutAction) {
this.checkoutAction.name = 'Checkout Process';
this.checkoutAction.startTime = checkoutStartTime;
this.checkoutAction.event_properties = {
'event_properties.cart_id': cart.id,
'event_properties.item_count': cart.items.length,
'event_properties.cart_total': cart.total,
'event_properties.currency': cart.currency,
'event_properties.checkout_step': 'started'
};
}
}
updateStep(step: string, additionalData: Record<string, any> = {}) {
if (this.checkoutAction?.state === 'active') {
// Preserve existing properties and add new ones
this.checkoutAction.event_properties = {
...this.checkoutAction.event_properties,
'event_properties.checkout_step': step,
...Object.entries(additionalData).reduce((acc, [key, value]) => {
// Remember to configure the properties on the server side!
acc[`event_properties.${key}`] = value;
return acc;
}, {} as Record<string, any>)
};
}
}
async processPayment(paymentMethod: string, amount: number) {
this.updateStep('payment', {
payment_method: paymentMethod,
payment_amount: amount
});
try {
const result = await submitPayment(paymentMethod, amount);
this.updateStep('payment_complete', {
transaction_id: result.transactionId,
payment_status: 'success'
});
return result;
} catch (error) {
this.updateStep('payment_failed', {
payment_status: 'failed',
error_code: error.code
});
throw error;
}
}
completeCheckout(orderId: string) {
if (this.checkoutAction?.state === 'active') {
this.updateStep('completed', {
order_id: orderId,
checkout_status: 'success'
});
this.checkoutAction.finish();
this.checkoutAction = undefined;
}
}
cancelCheckout(reason: string) {
if (this.checkoutAction?.state === 'active') {
this.updateStep('cancelled', {
checkout_status: 'cancelled',
cancellation_reason: reason
});
this.checkoutAction.finish();
this.checkoutAction = undefined;
}
}
}
// Usage
const tracker = new CheckoutTracker();
tracker.startCheckout(userCart);
tracker.updateStep('shipping_info', { shipping_method: 'express' });
tracker.updateStep('billing_info', { billing_country: 'US' });
await tracker.processPayment('credit_card', 99.99);
tracker.completeCheckout('ORDER-12345');
Unsubscribe from user action events when they are no longer needed to prevent memory leaks:
function setupUserActionMonitoring() {
const unsubscribe = dynatrace.userActions?.subscribe(userAction => {
// Handle user action
});
// Return cleanup function
return unsubscribe;
}
// Usage
const cleanup = setupUserActionMonitoring();
// Later...
cleanup();
Always check if the User Actions API is available before using it:
function createTrackedAction() {
// Check if userActions is available
if (!dynatrace.userActions) {
console.warn('User Actions module is not enabled');
return null;
}
return dynatrace.userActions.create();
}
Ensure user actions are always finished if autoClose is false, even when errors occur:
async function performTrackedOperation() {
const userAction = dynatrace.userActions?.create({ autoClose: false });
try {
await riskyOperation();
} catch (error) {
console.error('Operation failed:', error);
throw error;
} finally {
// Always finish the user action
userAction?.finish();
}
}
Creating excessive user actions can impact performance and data quality:
// ❌ Bad: Creating user actions for every keystroke
input.addEventListener('keypress', () => {
const userAction = dynatrace.userActions?.create();
// ...
});
// ✅ Good: Create user action for the complete interaction
let userAction: UserActionTracker | undefined;
input.addEventListener('focus', () => {
userAction = dynatrace.userActions?.create({ autoClose: false });
});
input.addEventListener('blur', () => {
userAction?.finish();
userAction = undefined;
});
Always verify a user action is still active before modifying its properties:
async function updateUserActionSafely(userAction: UserActionTracker | undefined, updates: Record<string, any>) {
// Only update if the action exists and is still active
if (userAction && userAction.state === 'active') {
userAction.event_properties = {
...userAction.event_properties,
...updates
};
}
}
When updating properties, always spread existing properties to avoid losing data in case you add properties on multiple locations:
// ❌ Bad: Overwrites all existing properties
if (userAction) {
userAction.event_properties = {
'event_properties.new_property': 'value'
};
}
// ✅ Good: Preserves existing properties
if (userAction) {
userAction.event_properties = {
...userAction.event_properties,
'event_properties.new_property': 'value'
};
}
The startTime must not be set to a future timestamp:
// ❌ Bad: Setting future timestamp
const userAction = dynatrace.userActions?.create();
if (userAction) {
userAction.startTime = Date.now() + 1000; // Invalid!
}
// ✅ Good: Setting past or current timestamp
const actualStart = Date.now();
// ... do some setup work ...
const userAction = dynatrace.userActions?.create();
if (userAction) {
userAction.startTime = actualStart; // Valid
}
If your user actions aren't appearing in Dynatrace:
dynatrace.userActions is definedfinish() on user actions with autoClose: falseIf user actions are completing before your operations finish:
// Ensure autoClose is set to false
const userAction = dynatrace.userActions?.create({ autoClose: false });
// Make sure to await all async operations
await Promise.all([
operation1(),
operation2(),
operation3()
]);
// Then finish
userAction?.finish();
If automatic detection interferes with manual control:
// Disable automatic detection at application startup
function initializeApp() {
// Disable automatic user action detection
dynatrace.userActions?.setAutomaticDetection(false);
// Set up your custom user action handling
setupCustomUserActionHandling();
}
If custom properties aren't showing up:
event_properties. prefixfinish()'active' when setting propertiesconst userAction = dynatrace.userActions?.create({ autoClose: false });
if (userAction) {
// ✅ Correct: Using proper prefix and supported types
userAction.event_properties = {
'event_properties.user_id': '12345', // string
'event_properties.item_count': 3, // number
'event_properties.is_premium': true // boolean
};
// Verify state before setting properties
console.log('User action state:', userAction.state);
// Complete the action
userAction.finish();
}
For complete API documentation, see the RUM JavaScript Typedoc.
@dynatrace/rum-javascript-sdk/api/user-actions)Safe wrappers that gracefully handle cases where the RUM JavaScript or User Actions module is not available:
create(options?): UserActionTracker | undefined
Creates a new user action. Returns undefined if the module is not available.
import { create } from '@dynatrace/rum-javascript-sdk/api/user-actions';
const userAction = create({ autoClose: false });
userAction?.finish();
subscribe(subscriber): Unsubscriber | undefined
Subscribes to all user action creation events. Returns undefined if the module is not available.
import { subscribe } from '@dynatrace/rum-javascript-sdk/api/user-actions';
const unsubscribe = subscribe((userAction) => {
console.log('User action created');
});
unsubscribe?.();
setAutomaticDetection(enabled): void
Enables or disables automatic user action detection. No-op if the module is not available.
import { setAutomaticDetection } from '@dynatrace/rum-javascript-sdk/api/user-actions';
setAutomaticDetection(false);
getCurrent(): UserActionTracker | undefined
Returns the currently active user action, or undefined if none is active or the module is not available.
import { getCurrent } from '@dynatrace/rum-javascript-sdk/api/user-actions';
const current = getCurrent();
if (current) {
current.autoClose = false;
}
@dynatrace/rum-javascript-sdk/api/promises/user-actions)Promise-based wrappers that wait for the module to become available or throw a DynatraceError:
create(options?, timeout?): Promise<UserActionTracker>
Creates a new user action, waiting up to timeout ms (default: 10000) for the module to be available.
import { create } from '@dynatrace/rum-javascript-sdk/api/promises/user-actions';
try {
const userAction = await create({ autoClose: false });
userAction.finish();
} catch (error) {
console.error('User Actions module not available');
}
subscribe(subscriber, timeout?): Promise<Unsubscriber>
setAutomaticDetection(enabled, timeout?): Promise<void>
getCurrent(timeout?): Promise<UserActionTracker | undefined>
All async functions follow the same pattern, accepting an optional timeout parameter.
| Property | Type | Access | Description |
|---|---|---|---|
finish() |
Function | - | Completes the user action and sends the event |
subscribe(callback) |
Function | - | Subscribes to automatic completion events |
autoClose |
boolean |
Get/Set | Controls whether the action closes automatically |
state |
'active' | 'complete' |
Get | Returns the current state of the user action |
event_properties |
Record<string, string | number | boolean> |
Get/Set | Custom properties for business context |
startTime |
number |
Get/Set | Start time in milliseconds (must not be in future) |
name |
string | undefined |
Get/Set | Display name for the user action |
UserActions - The main interface for user action management
create(options?) - Creates a new user actionsubscribe(subscriber) - Subscribes to all user action creation eventssetAutomaticDetection(enabled) - Enables or disables automatic detectioncurrent - Access the currently active user actionUserActionTracker - Represents an individual user action (see properties table above)UserActionStartOptions - Configuration options for creating user actions
autoClose?: boolean - Whether the action should close automatically (default: true)Unsubscriber - Function type () => void for unsubscribing from events📝 Note