RUM JavaScript API - 1.345.1
    Preparing search index...

    User Actions API

    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

    The User Actions API is experimental and may change in future releases. The dynatrace.userActions property is undefined if the User Actions module is disabled.

    With the User Actions API, you can:

    • Create custom user actions to track specific workflows
    • Control when user actions start and complete
    • Customize user action names for better identification
    • Attach custom properties to capture business context
    • Adjust timing by setting the start time
    • Monitor the state of active user actions
    • Subscribe to user action events and completion reasons
    • Disable automatic detection for manual control

    Before using the User Actions API, ensure that:

    • RUM JavaScript is properly configured and running
    • The User Actions module is enabled in your configuration
    • You have access to the dynatrace global object

    RUM 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 complete() 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 reached the maximum allowed duration and was forcefully completed to prevent indefinitely running actions. This reason is also reported when a duration would otherwise exceed the maximum and is capped to it — for example after a backdated startTime, or a long pause in JavaScript execution such as a backgrounded tab.

    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:

    Access the API directly through the global dynatrace.userActions namespace:

    const userAction = dynatrace?.userActions?.create({ customName: "My action" });
    userAction?.complete();

    This approach requires optional chaining (?.) to handle cases where RUM JavaScript or the module is not loaded.

    Use the safe wrapper functions from SDK.userActions (@dynatrace/rum-javascript-sdk/api/user-actions) that gracefully handle missing modules:

    import { create } from '@dynatrace/rum-javascript-sdk/api/user-actions';

    const userAction = create({ customName: "My action", completeAutomatically: false });
    // No need for optional chaining - returns undefined if module is unavailable
    userAction?.complete();

    Use the promise-based API from SDK.promises.userActions (@dynatrace/rum-javascript-sdk/api/promises/user-actions) 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({ customName: "My action", completeAutomatically: false });
    // Guaranteed to have a user action or will throw
    userAction.complete();
    } catch (error) {
    console.error('User Actions module not available:', error);
    }

    💡 Tip

    The examples in this guide use the direct API for brevity, but the safe wrapper API is recommended for production code as it provides better error handling and doesn't require optional chaining.

    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 user action and complete it manually:

    // Create a new user action — customName is required
    const userAction = dynatrace.userActions?.create({ customName: "Perform work" });

    // Perform your application logic here
    performSomeWork();

    // Manually complete the user action
    userAction?.complete();

    By default, completeAutomatically is false for API-created user actions, so you must call complete() explicitly. To let Dynatrace determine completion automatically based on activity (DOM mutations, network requests), pass completeAutomatically: true.

    🚨 Caution

    Calling complete() 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 complete() is called, leading to unreliable action durations. See 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-complete
    const userAction = dynatrace.userActions?.create({ customName: "Fetch and process", completeAutomatically: false });

    // Execute async operations
    await fetchData();
    await processResults();

    // Complete the user action when ready
    userAction?.complete();

    ⚠️ Warning

    When completeAutomatically is set to false, you must call complete() 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({ customName: "Navigate home", completeAutomatically: false });

    // Subscribe to completion events
    const unsubscribe = userAction?.subscribe(currentUserAction => {
    // This is just an informational log - we intentionally ignore automatic closing. We complete the user action manually later on.
    console.log(`User action would have been completed automatically`);
    });

    // Perform your operations
    await router.navigate('/home');

    // Complete the user action
    userAction?.complete();

    // Clean up the subscription when done
    unsubscribe?.();

    Set a custom name for your user action to make it more identifiable in Dynatrace. customName is required and must be provided when calling create():

    const userAction = dynatrace.userActions?.create({
    customName: 'Checkout Flow - Payment Processing',
    completeAutomatically: false
    });

    // Perform payment processing
    await processPayment();

    userAction?.complete();

    You can also update the name after creation using the customName setter:

    const userAction = dynatrace.userActions?.create({ customName: 'Checkout', completeAutomatically: false });

    if (userAction) {
    // Refine the name after determining the payment method
    userAction.customName = 'Checkout Flow - Payment Processing';
    }

    await processPayment();
    userAction?.complete();

    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

    Using 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({ customName: "Fetch data" });

    // 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.completeAutomatically = false;

    // Perform custom logic
    performCustomTracking(userAction);

    // Manually control completion
    setTimeout(() => {
    userAction.complete();
    }, 5000);
    });

    Apply different behavior based on the user action context:

    dynatrace.userActions?.subscribe(userAction => {
    // Only modify actions with name Add to Cart
    if (userAction.customName === 'Add to Cart') {
    userAction.completeAutomatically = false;

    // Add custom completion logic
    whenCustomConditionMet().then(() => {
    userAction.complete();
    });
    }
    });

    // some time later
    dynatrace.userActions?.create({ customName: "Add to Cart"});

    Pause automatic user action detection when it interferes with manual user action handling. This is particularly useful for single-page applications with complex routing logic.

    Changing automatic detection only affects the creation of future automatic user actions. It does not complete, interrupt, or otherwise affect the currently active user action. The active action continues tracking activity and follows its configured completion behavior.

    // 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({ customName: `Navigate to ${route}`, completeAutomatically: false });

    // Navigate without triggering automatic user action
    await router.navigate(route);

    // Manually complete when ready
    userAction?.complete();
    }

    🚨 Caution

    While automatic detection is disabled, no new user actions are created automatically. You must create new user actions manually using the 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.completeAutomatically = false;
    }

    // Perform async operation
    const response = await channel.send(message);

    // Manually complete the user action
    currentUserAction?.complete();

    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 route = target.getAttribute('data-navigate');
    const userAction = dynatrace.userActions?.create({
    customName: `Navigate to ${route}`
    });

    // Set additional properties
    if (userAction) {
    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 complete the user action
    userAction?.complete();
    }
    });

    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({
    customName: 'Complex Data Workflow'
    });

    if (userAction) {
    // Set up user action metadata
    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?.complete();
    }
    }

    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({
    customName: 'Data Processing Operation'
    });

    if (userAction) {
    // Set the start time to when the actual work began
    userAction.startTime = actualStartTime;
    }

    // Perform the actual measured work
    await processData();

    userAction?.complete();

    📝 Note

    A user action's maximum duration is measured from its startTime, not from when it was created — so setting an earlier start time consumes part of that budget and changes when the action times out:

    • If the start time is backdated far enough that the maximum duration is already exceeded, the action completes immediately with user_action.complete_reason: timeout.
    • Otherwise the maximum-duration timeout is re-aligned to fire at startTime + maximum duration, instead of running the full maximum from the creation time.
    • The reported duration can never exceed the maximum allowed duration. If a backdated start time — or a long pause in JavaScript execution, such as a backgrounded tab or sleeping device — would push it over, the duration is capped to the maximum and user_action.complete_reason is set to timeout.

    Set the start time to a past timestamp marking when the real work began. Future start times are rejected — see Don't set startTime in the future.

    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) => {
    // Pass completeAutomatically: true to allow Dynatrace to determine when the navigation is complete
    const userAction = dynatrace.userActions?.create({
    customName: `Route: ${path}`,
    completeAutomatically: true
    });

    // Add metadata if provided
    if (userAction) {
    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({
    customName: 'Checkout Process'
    });

    if (this.checkoutAction) {
    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.complete();
    this.checkoutAction = undefined;
    }
    }

    cancelCheckout(reason: string) {
    if (this.checkoutAction?.state === 'active') {
    this.updateStep('cancelled', {
    checkout_status: 'cancelled',
    cancellation_reason: reason
    });

    this.checkoutAction.complete();
    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');

    Custom activity trackers let you keep a user action open until your own async work finishes, and optionally contribute event_properties.* fields to the final event. Typical use cases: waiting for GraphQL responses, WebSocket round-trips, or any work the agent cannot observe automatically.

    A tracker is a creator function the RUM JavaScript calls synchronously at registration time:

    import type { UserActionActivityTrackerCreator } from "@dynatrace/rum-javascript-sdk/types/user-actions";

    const myTracker: UserActionActivityTrackerCreator = (context) => {
    // context.actionId — current user action id
    // context.block() — keep the action open
    // context.release() — allow it to complete again
    // context.notifyActivity() — extend the quiet window without blocking

    return {
    stop: () => {
    // Called when the action completes — clean up subscriptions here
    }
    };
    };

    At action creation:

    const action = dynatrace.userActions?.create({
    customName: "My tracked action",
    customTrackers: [{ name: "my-tracker", create: myTracker }]
    });

    After creation:

    const action = dynatrace.userActions?.create({ customName: "My tracked action" });
    action?.registerCustomTracker("my-tracker", myTracker);

    registerCustomTracker can be called any time while the action is active. Calling it on a completed action is a no-op.

    Call context.block() to prevent automatic completion. Call context.release() when your work finishes. Both are idempotent:

    const graphqlTracker: UserActionActivityTrackerCreator = (context) => {
    let pendingOperations = 0;

    const offStart = graphqlClient.on("operation:start", () => {
    pendingOperations++;
    context.block();
    });

    const offEnd = graphqlClient.on("operation:end", () => {
    if (pendingOperations === 0) return;
    pendingOperations--;
    if (pendingOperations === 0) {
    context.release();
    }
    });

    return { stop: () => { offStart(); offEnd(); } };
    };

    dynatrace.userActions?.create({
    customName: "GraphQL operation",
    customTrackers: [{ name: "graphql", create: graphqlTracker }]
    });

    Return a getEventProperties function alongside stop to attach fields to the user action event. Only keys prefixed with event_properties. are accepted:

    const graphqlTracker: UserActionActivityTrackerCreator = (context) => {
    let operationCount = 0;
    let pendingOperations = 0;

    const offStart = graphqlClient.on("operation:start", () => {
    operationCount++;
    pendingOperations++;
    context.block();
    });

    const offEnd = graphqlClient.on("operation:end", () => {
    if (pendingOperations === 0) return;
    pendingOperations--;
    if (pendingOperations === 0) {
    context.release();
    }
    });

    return {
    getEventProperties: () => ({
    "event_properties.graphql_operation_count": operationCount
    }),
    stop: () => { offStart(); offEnd(); }
    };
    };

    📝 Note

    getEventProperties() is called once when the action completes, before stop(). Properties set directly on the user action take precedence on key collisions. Configure properties on the server side before they appear in Dynatrace. See Event and session properties.

    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({ customName: "Tracked action" });
    }

    Ensure user actions are always completed if completeAutomatically is false, even when errors occur:

    async function performTrackedOperation() {
    const userAction = dynatrace.userActions?.create({
    customName: "Risky operation"
    });

    try {
    await riskyOperation();
    } catch (error) {
    console.error('Operation failed:', error);
    throw error;
    } finally {
    // Always complete the user action
    userAction?.complete();
    }
    }

    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({ customName: "Keypress" });
    // ...
    });

    // ✅ Good: Create user action for the complete interaction
    let userAction: UserActionTracker | undefined;

    input.addEventListener('focus', () => {
    userAction = dynatrace.userActions?.create({ customName: "Text input" });
    });

    input.addEventListener('blur', () => {
    userAction?.complete();
    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({ customName: "My operation" });
    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({ customName: "My operation" });
    if (userAction) {
    userAction.startTime = actualStart; // Valid
    }

    If your user actions aren't appearing in Dynatrace:

    1. Verify that the User Actions module is enabled
    2. Check that dynatrace.userActions is defined
    3. Ensure you're calling complete() on user actions with completeAutomatically: false
    4. Check browser console for any errors

    If user actions are completing before your operations finish:

    const userAction = dynatrace.userActions?.create({
    customName: "Parallel operations"
    });

    // Make sure to await all async operations
    await Promise.all([
    operation1(),
    operation2(),
    operation3()
    ]);

    // Then complete
    userAction?.complete();

    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:

    1. Check the console logs in your browser for any issues related to Dynatrace SDK usage.
    2. Ensure property keys start with event_properties. prefix
    3. Verify the property values are of supported types (string, number, boolean)
    4. Check that properties are set before calling complete()
    5. Make sure the user action state is 'active' when setting properties
    const userAction = dynatrace.userActions?.create({
    customName: "Tracked operation"
    });

    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.complete();
    }

    📝 Note

    This API is experimental and subject to change. Always check for updates when upgrading to new versions of the RUM JavaScript.