@dynatrace/rum-javascript-sdk 1.329.2 → 1.329.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -4
- package/docs/USERACTIONS.md +990 -0
- package/docs/testing.md +123 -0
- package/docs/types.md +44 -0
- package/package.json +110 -109
|
@@ -0,0 +1,990 @@
|
|
|
1
|
+
# User Actions API
|
|
2
|
+
|
|
3
|
+
**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.
|
|
4
|
+
|
|
5
|
+
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.
|
|
6
|
+
|
|
7
|
+
:::note
|
|
8
|
+
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.
|
|
9
|
+
:::
|
|
10
|
+
|
|
11
|
+
## What you can do
|
|
12
|
+
|
|
13
|
+
With the User Actions API, you can:
|
|
14
|
+
|
|
15
|
+
- **Create** custom user actions to track specific workflows
|
|
16
|
+
- **Control** when user actions start and finish
|
|
17
|
+
- **Customize** user action names for better identification
|
|
18
|
+
- **Attach** custom properties to capture business context
|
|
19
|
+
- **Adjust** timing by setting the start time
|
|
20
|
+
- **Monitor** the state of active user actions
|
|
21
|
+
- **Subscribe** to user action events and completion reasons
|
|
22
|
+
- **Disable** automatic detection for manual control
|
|
23
|
+
|
|
24
|
+
## Prerequisites
|
|
25
|
+
|
|
26
|
+
Before using the User Actions API, ensure that:
|
|
27
|
+
|
|
28
|
+
- RUM JavaScript is properly configured and running
|
|
29
|
+
- The User Actions module is enabled in your configuration
|
|
30
|
+
- You have access to the `dynatrace` global object
|
|
31
|
+
|
|
32
|
+
## User Actions
|
|
33
|
+
|
|
34
|
+
RUM JavaScript detects user interactions and processes that follow them. If a process follows a user interaction, Dynatrace
|
|
35
|
+
creates a user action for it. This user action lasts as long as there's activity on the page, like DOM mutations or
|
|
36
|
+
requests.
|
|
37
|
+
|
|
38
|
+
You can manipulate the behavior using the API described on this page.
|
|
39
|
+
|
|
40
|
+
### Rules
|
|
41
|
+
There can always only be one user action active at any point in time. Whenever a user action is active and a new one
|
|
42
|
+
is about to start, the active user action completes and the new one starts. The user action event provides information
|
|
43
|
+
about how the completion happened:
|
|
44
|
+
|
|
45
|
+
#### Completion reasons
|
|
46
|
+
|
|
47
|
+
User actions can complete for various reasons, indicated by the `user_action.complete_reason` property:
|
|
48
|
+
|
|
49
|
+
- **`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.
|
|
50
|
+
|
|
51
|
+
- **`completed_by_api`** - The user action was completed explicitly by calling the `finish()` function through the API.
|
|
52
|
+
|
|
53
|
+
- **`interrupted_by_api`** - The user action was interrupted when a new user action was created via the API using `create()`.
|
|
54
|
+
|
|
55
|
+
- **`interrupted_by_navigation`** - The user action was interrupted by a page navigation event, such as clicking a link or using browser navigation buttons.
|
|
56
|
+
|
|
57
|
+
- **`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.
|
|
58
|
+
|
|
59
|
+
- **`no_activity`** - The user action completed because there was no activity detected on the page. This only affects navigation user actions.
|
|
60
|
+
|
|
61
|
+
- **`page_hide`** - The user action completed because the page was dismissed (user navigated away, closed the tab, or the page hide event was fired).
|
|
62
|
+
|
|
63
|
+
- **`timeout`** - The user action exceeded the maximum allowed duration and was forcefully completed to prevent indefinitely running actions.
|
|
64
|
+
|
|
65
|
+
## API approaches
|
|
66
|
+
|
|
67
|
+
You can use the User Actions API directly, or via its synchronous or asynchronous SDK functions, matching the pattern established in the [@dynatrace/rum-javascript-sdk](../README.md):
|
|
68
|
+
|
|
69
|
+
### Direct API (via `dynatrace.userActions`)
|
|
70
|
+
|
|
71
|
+
Access the API directly through the global `dynatrace.userActions` namespace:
|
|
72
|
+
|
|
73
|
+
```typescript
|
|
74
|
+
const userAction = dynatrace?.userActions?.create();
|
|
75
|
+
userAction?.finish();
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
This approach requires optional chaining (`?.`) to handle cases where RUM JavaScript or the module is not loaded.
|
|
79
|
+
|
|
80
|
+
### Safe Wrapper API (recommended)
|
|
81
|
+
|
|
82
|
+
Use the safe wrapper functions from `@dynatrace/rum-javascript-sdk/api` that gracefully handle missing modules:
|
|
83
|
+
|
|
84
|
+
```typescript
|
|
85
|
+
import { create } from '@dynatrace/rum-javascript-sdk/api/user-actions';
|
|
86
|
+
|
|
87
|
+
const userAction = create({ autoClose: false });
|
|
88
|
+
// No need for optional chaining - returns undefined if module is unavailable
|
|
89
|
+
userAction?.finish();
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
### Async API
|
|
93
|
+
|
|
94
|
+
Use the promise-based API from `@dynatrace/rum-javascript-sdk/api/promises` when you need to ensure the module is available:
|
|
95
|
+
|
|
96
|
+
```typescript
|
|
97
|
+
import { create } from '@dynatrace/rum-javascript-sdk/api/promises/user-actions';
|
|
98
|
+
|
|
99
|
+
try {
|
|
100
|
+
const userAction = await create({ autoClose: false });
|
|
101
|
+
// Guaranteed to have a user action or will throw
|
|
102
|
+
userAction.finish();
|
|
103
|
+
} catch (error) {
|
|
104
|
+
console.error('User Actions module not available:', error);
|
|
105
|
+
}
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
:::tip
|
|
109
|
+
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.
|
|
110
|
+
:::
|
|
111
|
+
|
|
112
|
+
## Create custom user actions
|
|
113
|
+
|
|
114
|
+
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.
|
|
115
|
+
|
|
116
|
+
### Basic user action
|
|
117
|
+
|
|
118
|
+
Create a simple user action and finish it manually:
|
|
119
|
+
|
|
120
|
+
```typescript
|
|
121
|
+
// Create a new user action with default settings
|
|
122
|
+
const userAction = dynatrace.userActions?.create();
|
|
123
|
+
|
|
124
|
+
// Perform your application logic here
|
|
125
|
+
performSomeWork();
|
|
126
|
+
|
|
127
|
+
// Manually finish the user action
|
|
128
|
+
userAction?.finish();
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
By default, user actions are automatically closed when Dynatrace detects that the action is complete according to the RUM JavaScript user action [rules](#rules).
|
|
132
|
+
|
|
133
|
+
:::caution
|
|
134
|
+
Calling `finish()` is reliable in this case because `performSomeWork()` is synchronous. If it was awaited, there is no
|
|
135
|
+
guarantee that the action would still be open when `finish()` is called, leading to unreliable action durations. See
|
|
136
|
+
(User action with manual control)[#user-action-with-manual-control] below.
|
|
137
|
+
:::
|
|
138
|
+
|
|
139
|
+
### User action with manual control
|
|
140
|
+
|
|
141
|
+
Disable automatic closing to maintain full control over when the user action completes:
|
|
142
|
+
|
|
143
|
+
```typescript
|
|
144
|
+
// Create a user action that won't auto-close
|
|
145
|
+
const userAction = dynatrace.userActions?.create({ autoClose: false });
|
|
146
|
+
|
|
147
|
+
// Execute async operations
|
|
148
|
+
await fetchData();
|
|
149
|
+
await processResults();
|
|
150
|
+
|
|
151
|
+
// Finish the user action when ready
|
|
152
|
+
userAction?.finish();
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
:::warning
|
|
156
|
+
When `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.
|
|
157
|
+
:::
|
|
158
|
+
|
|
159
|
+
### Track user action completion
|
|
160
|
+
|
|
161
|
+
Subscribe to user action completion events to understand when actions would have been completed automatically:
|
|
162
|
+
|
|
163
|
+
```typescript
|
|
164
|
+
const userAction = dynatrace.userActions?.create({ autoClose: false });
|
|
165
|
+
|
|
166
|
+
// Subscribe to completion events
|
|
167
|
+
const unsubscribe = userAction?.subscribe(currentUserAction => {
|
|
168
|
+
// This is just an informational log - we intentionally ignore automatic closing. We finish the user action manually later on.
|
|
169
|
+
console.log(`User action would have been completed automatically`);
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
// Perform your operations
|
|
173
|
+
await router.navigate('/home');
|
|
174
|
+
|
|
175
|
+
// Finish the user action
|
|
176
|
+
userAction?.finish();
|
|
177
|
+
|
|
178
|
+
// Clean up the subscription when done
|
|
179
|
+
unsubscribe?.();
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
### Customize user action name
|
|
183
|
+
|
|
184
|
+
Set a custom name for your user action to make it more identifiable in Dynatrace:
|
|
185
|
+
|
|
186
|
+
```typescript
|
|
187
|
+
const userAction = dynatrace.userActions?.create({ autoClose: false });
|
|
188
|
+
|
|
189
|
+
// Set a descriptive name
|
|
190
|
+
if (userAction) {
|
|
191
|
+
userAction.name = 'Checkout Flow - Payment Processing';
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// Perform payment processing
|
|
195
|
+
await processPayment();
|
|
196
|
+
|
|
197
|
+
userAction?.finish();
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
### Add custom properties
|
|
201
|
+
|
|
202
|
+
Attach custom properties to user actions to provide additional context:
|
|
203
|
+
|
|
204
|
+
```typescript
|
|
205
|
+
const userAction = dynatrace.userActions?.current;
|
|
206
|
+
|
|
207
|
+
if (userAction) {
|
|
208
|
+
// Set custom properties
|
|
209
|
+
userAction.event_properties = {
|
|
210
|
+
'event_properties.transaction_id': 'TXN-123456',
|
|
211
|
+
'event_properties.payment_method': 'credit_card',
|
|
212
|
+
'event_properties.amount': 99.99,
|
|
213
|
+
'event_properties.currency': 'USD',
|
|
214
|
+
'event_properties.is_first_purchase': true
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
Or use the `addEventModifier` API to attach them to events directly:
|
|
220
|
+
```typescript
|
|
221
|
+
dynatrace.addEventModifier(evt => {
|
|
222
|
+
if (evt.has_user_action) {
|
|
223
|
+
return {
|
|
224
|
+
...evt,
|
|
225
|
+
'event_properties.transaction_id': 'TXN-123456',
|
|
226
|
+
'event_properties.payment_method': 'credit_card',
|
|
227
|
+
'event_properties.amount': 99.99,
|
|
228
|
+
'event_properties.currency': 'USD',
|
|
229
|
+
'event_properties.is_first_purchase': true
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
return evt;
|
|
233
|
+
})
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
:::note
|
|
237
|
+
Using `event_properties` requires you to configure them in the Experience Vitals app. Non-configured properties are
|
|
238
|
+
discarded on the server side. Select your Frontend, go to the Settings-Tab and configure _Event and session properties_.
|
|
239
|
+
:::
|
|
240
|
+
|
|
241
|
+
### Check user action state
|
|
242
|
+
|
|
243
|
+
Monitor the state of a user action to determine if it's still active:
|
|
244
|
+
|
|
245
|
+
```typescript
|
|
246
|
+
const userAction = dynatrace.userActions?.create();
|
|
247
|
+
|
|
248
|
+
// Perform some operations
|
|
249
|
+
await fetchData();
|
|
250
|
+
|
|
251
|
+
// Check if the user action is still active
|
|
252
|
+
if (userAction?.state === 'active') {
|
|
253
|
+
console.log('User action is still running');
|
|
254
|
+
|
|
255
|
+
// Continue with more operations
|
|
256
|
+
await processData();
|
|
257
|
+
} else {
|
|
258
|
+
console.log('User action has already completed');
|
|
259
|
+
}
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
## Subscribe to user actions
|
|
263
|
+
|
|
264
|
+
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.
|
|
265
|
+
|
|
266
|
+
### Monitor all user actions
|
|
267
|
+
|
|
268
|
+
Subscribe to user action creation events:
|
|
269
|
+
|
|
270
|
+
```typescript
|
|
271
|
+
const unsubscribe = dynatrace.userActions?.subscribe(userAction => {
|
|
272
|
+
console.log('New user action created:', userAction);
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
// Later, when you no longer need to monitor user actions
|
|
276
|
+
unsubscribe?.();
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
### Modify user action behavior
|
|
280
|
+
|
|
281
|
+
Change how user actions behave by modifying their properties:
|
|
282
|
+
|
|
283
|
+
```typescript
|
|
284
|
+
dynatrace.userActions?.subscribe(userAction => {
|
|
285
|
+
// Disable automatic completion for all user actions
|
|
286
|
+
userAction.autoClose = false;
|
|
287
|
+
|
|
288
|
+
// Perform custom logic
|
|
289
|
+
performCustomTracking(userAction);
|
|
290
|
+
|
|
291
|
+
// Manually control completion
|
|
292
|
+
setTimeout(() => {
|
|
293
|
+
userAction.finish();
|
|
294
|
+
}, 5000);
|
|
295
|
+
});
|
|
296
|
+
```
|
|
297
|
+
|
|
298
|
+
### Conditional user action handling
|
|
299
|
+
|
|
300
|
+
Apply different behavior based on the user action context:
|
|
301
|
+
|
|
302
|
+
```typescript
|
|
303
|
+
dynatrace.userActions?.subscribe(userAction => {
|
|
304
|
+
// Only modify actions with name Add to Cart
|
|
305
|
+
if (userAction.name === 'Add to Cart') {
|
|
306
|
+
userAction.autoClose = false;
|
|
307
|
+
|
|
308
|
+
// Add custom completion logic
|
|
309
|
+
whenCustomConditionMet().then(() => {
|
|
310
|
+
userAction.finish();
|
|
311
|
+
});
|
|
312
|
+
}
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
// some time later
|
|
316
|
+
dynatrace.userActions?.create({ name: "Add to Cart"});
|
|
317
|
+
```
|
|
318
|
+
|
|
319
|
+
## Disable automatic detection
|
|
320
|
+
|
|
321
|
+
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.
|
|
322
|
+
|
|
323
|
+
### Basic usage
|
|
324
|
+
|
|
325
|
+
```typescript
|
|
326
|
+
// Disable automatic user action detection
|
|
327
|
+
dynatrace.userActions?.setAutomaticDetection(false);
|
|
328
|
+
|
|
329
|
+
// Now you have full control over user action creation
|
|
330
|
+
async function handleNavigation(route) {
|
|
331
|
+
const userAction = dynatrace.userActions?.create({ autoClose: false });
|
|
332
|
+
|
|
333
|
+
// Navigate without triggering automatic user action
|
|
334
|
+
await router.navigate(route);
|
|
335
|
+
|
|
336
|
+
// Manually finish when ready
|
|
337
|
+
userAction?.finish();
|
|
338
|
+
}
|
|
339
|
+
```
|
|
340
|
+
|
|
341
|
+
:::caution
|
|
342
|
+
When automatic detection is disabled, no user actions are created automatically. You must create all user actions manually using the `create()` function.
|
|
343
|
+
:::
|
|
344
|
+
|
|
345
|
+
### Re-enable automatic detection
|
|
346
|
+
|
|
347
|
+
```typescript
|
|
348
|
+
// Re-enable automatic user action detection
|
|
349
|
+
dynatrace.userActions?.setAutomaticDetection(true);
|
|
350
|
+
```
|
|
351
|
+
|
|
352
|
+
## Access current user action
|
|
353
|
+
|
|
354
|
+
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.
|
|
355
|
+
|
|
356
|
+
### Basic usage
|
|
357
|
+
|
|
358
|
+
```typescript
|
|
359
|
+
async function postMessage(message, channel) {
|
|
360
|
+
const currentUserAction = dynatrace.userActions?.current;
|
|
361
|
+
|
|
362
|
+
if (currentUserAction) {
|
|
363
|
+
// Prevent automatic completion
|
|
364
|
+
currentUserAction.autoClose = false;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
// Perform async operation
|
|
368
|
+
const response = await channel.send(message);
|
|
369
|
+
|
|
370
|
+
// Manually finish the user action
|
|
371
|
+
currentUserAction?.finish();
|
|
372
|
+
|
|
373
|
+
return response;
|
|
374
|
+
}
|
|
375
|
+
```
|
|
376
|
+
|
|
377
|
+
## Advanced scenarios
|
|
378
|
+
|
|
379
|
+
### Handle click events with custom routing
|
|
380
|
+
|
|
381
|
+
Prevent automatic user action completion when handling click events that trigger complex navigation:
|
|
382
|
+
|
|
383
|
+
```typescript
|
|
384
|
+
dynatrace.userActions?.setAutomaticDetection(false);
|
|
385
|
+
|
|
386
|
+
document.addEventListener('click', async (event) => {
|
|
387
|
+
const target = event.target as HTMLElement;
|
|
388
|
+
|
|
389
|
+
if (target.matches('[data-navigate]')) {
|
|
390
|
+
const userAction = dynatrace.userActions?.create({ autoClose: false });
|
|
391
|
+
|
|
392
|
+
const route = target.getAttribute('data-navigate');
|
|
393
|
+
|
|
394
|
+
// Set custom name and properties
|
|
395
|
+
if (userAction) {
|
|
396
|
+
userAction.name = `Navigate to ${route}`;
|
|
397
|
+
userAction.event_properties = {
|
|
398
|
+
'event_properties.target_route': route,
|
|
399
|
+
'event_properties.navigation_trigger': 'click'
|
|
400
|
+
};
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
// This would normally create an automatic user action
|
|
404
|
+
await router.redirect(route);
|
|
405
|
+
|
|
406
|
+
// Wait for the route to fully load
|
|
407
|
+
await waitForRouteReady();
|
|
408
|
+
|
|
409
|
+
// Now finish the user action
|
|
410
|
+
userAction?.finish();
|
|
411
|
+
}
|
|
412
|
+
});
|
|
413
|
+
```
|
|
414
|
+
|
|
415
|
+
### Coordinate multiple async operations
|
|
416
|
+
|
|
417
|
+
Create a user action that spans multiple asynchronous operations:
|
|
418
|
+
|
|
419
|
+
```typescript
|
|
420
|
+
async function handleComplexWorkflow(workflowId: string, priority: string) {
|
|
421
|
+
const startTime = Date.now();
|
|
422
|
+
const userAction = dynatrace.userActions?.create({ autoClose: false });
|
|
423
|
+
|
|
424
|
+
if (userAction) {
|
|
425
|
+
// Set up user action metadata
|
|
426
|
+
userAction.name = 'Complex Data Workflow';
|
|
427
|
+
userAction.startTime = startTime;
|
|
428
|
+
userAction.event_properties = {
|
|
429
|
+
'event_properties.workflow_id': workflowId,
|
|
430
|
+
'event_properties.priority': priority,
|
|
431
|
+
'event_properties.step_count': 3
|
|
432
|
+
};
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
try {
|
|
436
|
+
// Step 1: Fetch user data
|
|
437
|
+
const userData = await fetchUserData();
|
|
438
|
+
|
|
439
|
+
// Update properties after step 1
|
|
440
|
+
if (userAction && userAction.state === 'active') {
|
|
441
|
+
userAction.event_properties = {
|
|
442
|
+
...userAction.event_properties,
|
|
443
|
+
'event_properties.current_step': 1,
|
|
444
|
+
'event_properties.records_fetched': userData.length
|
|
445
|
+
};
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
// Step 2: Process data
|
|
449
|
+
const processedData = await processData(userData);
|
|
450
|
+
|
|
451
|
+
// Update properties after step 2
|
|
452
|
+
if (userAction && userAction.state === 'active') {
|
|
453
|
+
userAction.event_properties = {
|
|
454
|
+
...userAction.event_properties,
|
|
455
|
+
'event_properties.current_step': 2,
|
|
456
|
+
'event_properties.records_processed': processedData.length
|
|
457
|
+
};
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
// Step 3: Submit results
|
|
461
|
+
const result = await submitResults(processedData);
|
|
462
|
+
|
|
463
|
+
// Final update
|
|
464
|
+
if (userAction && userAction.state === 'active') {
|
|
465
|
+
userAction.event_properties = {
|
|
466
|
+
...userAction.event_properties,
|
|
467
|
+
'event_properties.current_step': 3,
|
|
468
|
+
'event_properties.workflow_status': 'completed',
|
|
469
|
+
'event_properties.result_id': result.id
|
|
470
|
+
};
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
} catch (error) {
|
|
474
|
+
console.error('Workflow failed:', error);
|
|
475
|
+
|
|
476
|
+
// Mark the workflow as failed in properties
|
|
477
|
+
if (userAction && userAction.state === 'active') {
|
|
478
|
+
userAction.event_properties = {
|
|
479
|
+
...userAction.event_properties,
|
|
480
|
+
'event_properties.workflow_status': 'failed',
|
|
481
|
+
'event_properties.error_message': error.message
|
|
482
|
+
};
|
|
483
|
+
}
|
|
484
|
+
} finally {
|
|
485
|
+
// make sure to complete the user action in any case
|
|
486
|
+
userAction?.finish();
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
```
|
|
490
|
+
|
|
491
|
+
### Adjust user action timing
|
|
492
|
+
|
|
493
|
+
Modify the start time of a user action for more accurate measurements:
|
|
494
|
+
|
|
495
|
+
```typescript
|
|
496
|
+
// Record the actual start time before any setup
|
|
497
|
+
const actualStartTime = Date.now();
|
|
498
|
+
|
|
499
|
+
// Do some setup work that shouldn't be part of the measurement
|
|
500
|
+
await loadConfiguration();
|
|
501
|
+
await initializeServices();
|
|
502
|
+
|
|
503
|
+
// Create the user action and set the start time
|
|
504
|
+
const userAction = dynatrace.userActions?.create({ autoClose: false });
|
|
505
|
+
|
|
506
|
+
if (userAction) {
|
|
507
|
+
// Set the start time to when the actual work began
|
|
508
|
+
userAction.startTime = actualStartTime;
|
|
509
|
+
userAction.name = 'Data Processing Operation';
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
// Perform the actual measured work
|
|
513
|
+
await processData();
|
|
514
|
+
|
|
515
|
+
userAction?.finish();
|
|
516
|
+
```
|
|
517
|
+
|
|
518
|
+
### Handle framework-specific routing
|
|
519
|
+
|
|
520
|
+
Integrate with popular frameworks like React Router:
|
|
521
|
+
|
|
522
|
+
```typescript
|
|
523
|
+
// Example with React Router
|
|
524
|
+
import { useEffect } from 'react';
|
|
525
|
+
import { useNavigate, useLocation } from 'react-router-dom';
|
|
526
|
+
|
|
527
|
+
function useManualUserActionTracking() {
|
|
528
|
+
const navigate = useNavigate();
|
|
529
|
+
const location = useLocation();
|
|
530
|
+
|
|
531
|
+
useEffect(() => {
|
|
532
|
+
// Disable automatic detection to prevent conflicts
|
|
533
|
+
dynatrace.userActions?.setAutomaticDetection(false);
|
|
534
|
+
}, []);
|
|
535
|
+
|
|
536
|
+
const trackedNavigate = async (path: string) => {
|
|
537
|
+
// Don't use autoClose here to allow Dynatrace to determine when the navigation is complete
|
|
538
|
+
const userAction = dynatrace.userActions?.create();
|
|
539
|
+
|
|
540
|
+
// Add metadata if provided
|
|
541
|
+
if (userAction) {
|
|
542
|
+
userAction.name = `Route: ${path}`;
|
|
543
|
+
userAction.event_properties = {
|
|
544
|
+
'event_properties.route': path,
|
|
545
|
+
'event_properties.previous_route': location.pathname
|
|
546
|
+
};
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
// Perform navigation
|
|
550
|
+
navigate(path);
|
|
551
|
+
};
|
|
552
|
+
|
|
553
|
+
return trackedNavigate;
|
|
554
|
+
}
|
|
555
|
+
```
|
|
556
|
+
|
|
557
|
+
### E-commerce checkout flow
|
|
558
|
+
|
|
559
|
+
A complete example tracking an e-commerce checkout with detailed metadata:
|
|
560
|
+
|
|
561
|
+
```typescript
|
|
562
|
+
class CheckoutTracker {
|
|
563
|
+
private checkoutAction?: UserActionTracker;
|
|
564
|
+
|
|
565
|
+
startCheckout(cart: Cart) {
|
|
566
|
+
// Record the actual start time
|
|
567
|
+
const checkoutStartTime = Date.now();
|
|
568
|
+
|
|
569
|
+
this.checkoutAction = dynatrace.userActions?.create({ autoClose: false });
|
|
570
|
+
|
|
571
|
+
if (this.checkoutAction) {
|
|
572
|
+
this.checkoutAction.name = 'Checkout Process';
|
|
573
|
+
this.checkoutAction.startTime = checkoutStartTime;
|
|
574
|
+
this.checkoutAction.event_properties = {
|
|
575
|
+
'event_properties.cart_id': cart.id,
|
|
576
|
+
'event_properties.item_count': cart.items.length,
|
|
577
|
+
'event_properties.cart_total': cart.total,
|
|
578
|
+
'event_properties.currency': cart.currency,
|
|
579
|
+
'event_properties.checkout_step': 'started'
|
|
580
|
+
};
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
updateStep(step: string, additionalData: Record<string, any> = {}) {
|
|
585
|
+
if (this.checkoutAction?.state === 'active') {
|
|
586
|
+
// Preserve existing properties and add new ones
|
|
587
|
+
this.checkoutAction.event_properties = {
|
|
588
|
+
...this.checkoutAction.event_properties,
|
|
589
|
+
'event_properties.checkout_step': step,
|
|
590
|
+
...Object.entries(additionalData).reduce((acc, [key, value]) => {
|
|
591
|
+
// Remember to configure the properties in the Experience Vitals app!
|
|
592
|
+
acc[`event_properties.${key}`] = value;
|
|
593
|
+
return acc;
|
|
594
|
+
}, {} as Record<string, any>)
|
|
595
|
+
};
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
async processPayment(paymentMethod: string, amount: number) {
|
|
600
|
+
this.updateStep('payment', {
|
|
601
|
+
payment_method: paymentMethod,
|
|
602
|
+
payment_amount: amount
|
|
603
|
+
});
|
|
604
|
+
|
|
605
|
+
try {
|
|
606
|
+
const result = await submitPayment(paymentMethod, amount);
|
|
607
|
+
|
|
608
|
+
this.updateStep('payment_complete', {
|
|
609
|
+
transaction_id: result.transactionId,
|
|
610
|
+
payment_status: 'success'
|
|
611
|
+
});
|
|
612
|
+
|
|
613
|
+
return result;
|
|
614
|
+
} catch (error) {
|
|
615
|
+
this.updateStep('payment_failed', {
|
|
616
|
+
payment_status: 'failed',
|
|
617
|
+
error_code: error.code
|
|
618
|
+
});
|
|
619
|
+
throw error;
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
completeCheckout(orderId: string) {
|
|
624
|
+
if (this.checkoutAction?.state === 'active') {
|
|
625
|
+
this.updateStep('completed', {
|
|
626
|
+
order_id: orderId,
|
|
627
|
+
checkout_status: 'success'
|
|
628
|
+
});
|
|
629
|
+
|
|
630
|
+
this.checkoutAction.finish();
|
|
631
|
+
this.checkoutAction = undefined;
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
cancelCheckout(reason: string) {
|
|
636
|
+
if (this.checkoutAction?.state === 'active') {
|
|
637
|
+
this.updateStep('cancelled', {
|
|
638
|
+
checkout_status: 'cancelled',
|
|
639
|
+
cancellation_reason: reason
|
|
640
|
+
});
|
|
641
|
+
|
|
642
|
+
this.checkoutAction.finish();
|
|
643
|
+
this.checkoutAction = undefined;
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
// Usage
|
|
649
|
+
const tracker = new CheckoutTracker();
|
|
650
|
+
tracker.startCheckout(userCart);
|
|
651
|
+
tracker.updateStep('shipping_info', { shipping_method: 'express' });
|
|
652
|
+
tracker.updateStep('billing_info', { billing_country: 'US' });
|
|
653
|
+
await tracker.processPayment('credit_card', 99.99);
|
|
654
|
+
tracker.completeCheckout('ORDER-12345');
|
|
655
|
+
```
|
|
656
|
+
|
|
657
|
+
## Best practices
|
|
658
|
+
|
|
659
|
+
### Always clean up subscriptions
|
|
660
|
+
|
|
661
|
+
Unsubscribe from user action events when they are no longer needed to prevent memory leaks:
|
|
662
|
+
|
|
663
|
+
```typescript
|
|
664
|
+
function setupUserActionMonitoring() {
|
|
665
|
+
const unsubscribe = dynatrace.userActions?.subscribe(userAction => {
|
|
666
|
+
// Handle user action
|
|
667
|
+
});
|
|
668
|
+
|
|
669
|
+
// Return cleanup function
|
|
670
|
+
return unsubscribe;
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
// Usage
|
|
674
|
+
const cleanup = setupUserActionMonitoring();
|
|
675
|
+
// Later...
|
|
676
|
+
cleanup();
|
|
677
|
+
```
|
|
678
|
+
|
|
679
|
+
### Handle undefined gracefully
|
|
680
|
+
|
|
681
|
+
Always check if the User Actions API is available before using it:
|
|
682
|
+
|
|
683
|
+
```typescript
|
|
684
|
+
function createTrackedAction() {
|
|
685
|
+
// Check if userActions is available
|
|
686
|
+
if (!dynatrace.userActions) {
|
|
687
|
+
console.warn('User Actions module is not enabled');
|
|
688
|
+
return null;
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
return dynatrace.userActions.create();
|
|
692
|
+
}
|
|
693
|
+
```
|
|
694
|
+
|
|
695
|
+
### Finish user actions in finally blocks
|
|
696
|
+
|
|
697
|
+
Ensure user actions are always finished if `autoClose` is `false`, even when errors occur:
|
|
698
|
+
|
|
699
|
+
```typescript
|
|
700
|
+
async function performTrackedOperation() {
|
|
701
|
+
const userAction = dynatrace.userActions?.create({ autoClose: false });
|
|
702
|
+
|
|
703
|
+
try {
|
|
704
|
+
await riskyOperation();
|
|
705
|
+
} catch (error) {
|
|
706
|
+
console.error('Operation failed:', error);
|
|
707
|
+
throw error;
|
|
708
|
+
} finally {
|
|
709
|
+
// Always finish the user action
|
|
710
|
+
userAction?.finish();
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
```
|
|
714
|
+
|
|
715
|
+
### Avoid creating too many user actions
|
|
716
|
+
|
|
717
|
+
Creating excessive user actions can impact performance and data quality:
|
|
718
|
+
|
|
719
|
+
```typescript
|
|
720
|
+
// ❌ Bad: Creating user actions for every keystroke
|
|
721
|
+
input.addEventListener('keypress', () => {
|
|
722
|
+
const userAction = dynatrace.userActions?.create();
|
|
723
|
+
// ...
|
|
724
|
+
});
|
|
725
|
+
|
|
726
|
+
// ✅ Good: Create user action for the complete interaction
|
|
727
|
+
let userAction: UserActionTracker | undefined;
|
|
728
|
+
|
|
729
|
+
input.addEventListener('focus', () => {
|
|
730
|
+
userAction = dynatrace.userActions?.create({ autoClose: false });
|
|
731
|
+
});
|
|
732
|
+
|
|
733
|
+
input.addEventListener('blur', () => {
|
|
734
|
+
userAction?.finish();
|
|
735
|
+
userAction = undefined;
|
|
736
|
+
});
|
|
737
|
+
```
|
|
738
|
+
|
|
739
|
+
### Check state before modifying completed actions
|
|
740
|
+
|
|
741
|
+
Always verify a user action is still active before modifying its properties:
|
|
742
|
+
|
|
743
|
+
```typescript
|
|
744
|
+
async function updateUserActionSafely(userAction: UserActionTracker | undefined, updates: Record<string, any>) {
|
|
745
|
+
// Only update if the action exists and is still active
|
|
746
|
+
if (userAction && userAction.state === 'active') {
|
|
747
|
+
userAction.event_properties = {
|
|
748
|
+
...userAction.event_properties,
|
|
749
|
+
...updates
|
|
750
|
+
};
|
|
751
|
+
}
|
|
752
|
+
}
|
|
753
|
+
```
|
|
754
|
+
|
|
755
|
+
### Preserve existing properties when updating
|
|
756
|
+
|
|
757
|
+
When updating properties, always spread existing properties to avoid losing data in case you add properties on multiple locations:
|
|
758
|
+
|
|
759
|
+
```typescript
|
|
760
|
+
// ❌ Bad: Overwrites all existing properties
|
|
761
|
+
if (userAction) {
|
|
762
|
+
userAction.event_properties = {
|
|
763
|
+
'event_properties.new_property': 'value'
|
|
764
|
+
};
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
// ✅ Good: Preserves existing properties
|
|
768
|
+
if (userAction) {
|
|
769
|
+
userAction.event_properties = {
|
|
770
|
+
...userAction.event_properties,
|
|
771
|
+
'event_properties.new_property': 'value'
|
|
772
|
+
};
|
|
773
|
+
}
|
|
774
|
+
```
|
|
775
|
+
|
|
776
|
+
### Don't set startTime in the future
|
|
777
|
+
|
|
778
|
+
The `startTime` must not be set to a future timestamp:
|
|
779
|
+
|
|
780
|
+
```typescript
|
|
781
|
+
// ❌ Bad: Setting future timestamp
|
|
782
|
+
const userAction = dynatrace.userActions?.create();
|
|
783
|
+
if (userAction) {
|
|
784
|
+
userAction.startTime = Date.now() + 1000; // Invalid!
|
|
785
|
+
}
|
|
786
|
+
```
|
|
787
|
+
```typescript
|
|
788
|
+
// ✅ Good: Setting past or current timestamp
|
|
789
|
+
const actualStart = Date.now();
|
|
790
|
+
// ... do some setup work ...
|
|
791
|
+
const userAction = dynatrace.userActions?.create();
|
|
792
|
+
if (userAction) {
|
|
793
|
+
userAction.startTime = actualStart; // Valid
|
|
794
|
+
}
|
|
795
|
+
```
|
|
796
|
+
|
|
797
|
+
## Troubleshooting
|
|
798
|
+
|
|
799
|
+
### User actions not appearing
|
|
800
|
+
|
|
801
|
+
If your user actions aren't appearing in Dynatrace:
|
|
802
|
+
|
|
803
|
+
1. Verify that the User Actions module is enabled
|
|
804
|
+
2. Check that `dynatrace.userActions` is defined
|
|
805
|
+
3. Ensure you're calling `finish()` on user actions with `autoClose: false`
|
|
806
|
+
4. Check browser console for any errors
|
|
807
|
+
|
|
808
|
+
### User actions completing too early
|
|
809
|
+
|
|
810
|
+
If user actions are completing before your operations finish:
|
|
811
|
+
|
|
812
|
+
```typescript
|
|
813
|
+
// Ensure autoClose is set to false
|
|
814
|
+
const userAction = dynatrace.userActions?.create({ autoClose: false });
|
|
815
|
+
|
|
816
|
+
// Make sure to await all async operations
|
|
817
|
+
await Promise.all([
|
|
818
|
+
operation1(),
|
|
819
|
+
operation2(),
|
|
820
|
+
operation3()
|
|
821
|
+
]);
|
|
822
|
+
|
|
823
|
+
// Then finish
|
|
824
|
+
userAction?.finish();
|
|
825
|
+
```
|
|
826
|
+
|
|
827
|
+
### Conflicts with automatic detection
|
|
828
|
+
|
|
829
|
+
If automatic detection interferes with manual control:
|
|
830
|
+
|
|
831
|
+
```typescript
|
|
832
|
+
// Disable automatic detection at application startup
|
|
833
|
+
function initializeApp() {
|
|
834
|
+
// Disable automatic user action detection
|
|
835
|
+
dynatrace.userActions?.setAutomaticDetection(false);
|
|
836
|
+
|
|
837
|
+
// Set up your custom user action handling
|
|
838
|
+
setupCustomUserActionHandling();
|
|
839
|
+
}
|
|
840
|
+
```
|
|
841
|
+
|
|
842
|
+
### Properties not appearing in Dynatrace
|
|
843
|
+
|
|
844
|
+
If custom properties aren't showing up:
|
|
845
|
+
|
|
846
|
+
1. Check the console logs in your browser for any issues related to Dynatrace SDK usage.
|
|
847
|
+
2. Ensure property keys start with `event_properties.` prefix
|
|
848
|
+
3. Verify the property values are of supported types (string, number, boolean)
|
|
849
|
+
4. Check that properties are set before calling `finish()`
|
|
850
|
+
5. Make sure the user action state is `'active'` when setting properties
|
|
851
|
+
|
|
852
|
+
```typescript
|
|
853
|
+
const userAction = dynatrace.userActions?.create({ autoClose: false });
|
|
854
|
+
|
|
855
|
+
if (userAction) {
|
|
856
|
+
// ✅ Correct: Using proper prefix and supported types
|
|
857
|
+
userAction.event_properties = {
|
|
858
|
+
'event_properties.user_id': '12345', // string
|
|
859
|
+
'event_properties.item_count': 3, // number
|
|
860
|
+
'event_properties.is_premium': true // boolean
|
|
861
|
+
};
|
|
862
|
+
|
|
863
|
+
// Verify state before setting properties
|
|
864
|
+
console.log('User action state:', userAction.state);
|
|
865
|
+
|
|
866
|
+
// Complete the action
|
|
867
|
+
userAction.finish();
|
|
868
|
+
}
|
|
869
|
+
```
|
|
870
|
+
|
|
871
|
+
|
|
872
|
+
## API Reference
|
|
873
|
+
|
|
874
|
+
For complete API documentation, see the [RUM JavaScript Typedoc](https://www.dynatrace.com/support/doc/javascriptapi/doc-latest/).
|
|
875
|
+
|
|
876
|
+
### Safe Wrapper Functions
|
|
877
|
+
|
|
878
|
+
#### Synchronous API (`@dynatrace/rum-javascript-sdk/api/user-actions`)
|
|
879
|
+
|
|
880
|
+
Safe wrappers that gracefully handle cases where the RUM JavaScript or User Actions module is not available:
|
|
881
|
+
|
|
882
|
+
**`create(options?): UserActionTracker | undefined`**
|
|
883
|
+
|
|
884
|
+
Creates a new user action. Returns `undefined` if the module is not available.
|
|
885
|
+
|
|
886
|
+
```typescript
|
|
887
|
+
import { create } from '@dynatrace/rum-javascript-sdk/api/user-actions';
|
|
888
|
+
|
|
889
|
+
const userAction = create({ autoClose: false });
|
|
890
|
+
userAction?.finish();
|
|
891
|
+
```
|
|
892
|
+
|
|
893
|
+
**`subscribe(subscriber): Unsubscriber | undefined`**
|
|
894
|
+
|
|
895
|
+
Subscribes to all user action creation events. Returns `undefined` if the module is not available.
|
|
896
|
+
|
|
897
|
+
```typescript
|
|
898
|
+
import { subscribe } from '@dynatrace/rum-javascript-sdk/api/user-actions';
|
|
899
|
+
|
|
900
|
+
const unsubscribe = subscribe((userAction) => {
|
|
901
|
+
console.log('User action created');
|
|
902
|
+
});
|
|
903
|
+
unsubscribe?.();
|
|
904
|
+
```
|
|
905
|
+
|
|
906
|
+
**`setAutomaticDetection(enabled): void`**
|
|
907
|
+
|
|
908
|
+
Enables or disables automatic user action detection. No-op if the module is not available.
|
|
909
|
+
|
|
910
|
+
```typescript
|
|
911
|
+
import { setAutomaticDetection } from '@dynatrace/rum-javascript-sdk/api/user-actions';
|
|
912
|
+
|
|
913
|
+
setAutomaticDetection(false);
|
|
914
|
+
```
|
|
915
|
+
|
|
916
|
+
**`getCurrent(): UserActionTracker | undefined`**
|
|
917
|
+
|
|
918
|
+
Returns the currently active user action, or `undefined` if none is active or the module is not available.
|
|
919
|
+
|
|
920
|
+
```typescript
|
|
921
|
+
import { getCurrent } from '@dynatrace/rum-javascript-sdk/api/user-actions';
|
|
922
|
+
|
|
923
|
+
const current = getCurrent();
|
|
924
|
+
if (current) {
|
|
925
|
+
current.autoClose = false;
|
|
926
|
+
}
|
|
927
|
+
```
|
|
928
|
+
|
|
929
|
+
#### Asynchronous API (`@dynatrace/rum-javascript-sdk/api/promises/user-actions`)
|
|
930
|
+
|
|
931
|
+
Promise-based wrappers that wait for the module to become available or throw a `DynatraceError`:
|
|
932
|
+
|
|
933
|
+
**`create(options?, timeout?): Promise<UserActionTracker>`**
|
|
934
|
+
|
|
935
|
+
Creates a new user action, waiting up to `timeout` ms (default: 10000) for the module to be available.
|
|
936
|
+
|
|
937
|
+
```typescript
|
|
938
|
+
import { create } from '@dynatrace/rum-javascript-sdk/api/promises/user-actions';
|
|
939
|
+
|
|
940
|
+
try {
|
|
941
|
+
const userAction = await create({ autoClose: false });
|
|
942
|
+
userAction.finish();
|
|
943
|
+
} catch (error) {
|
|
944
|
+
console.error('User Actions module not available');
|
|
945
|
+
}
|
|
946
|
+
```
|
|
947
|
+
|
|
948
|
+
**`subscribe(subscriber, timeout?): Promise<Unsubscriber>`**
|
|
949
|
+
|
|
950
|
+
**`setAutomaticDetection(enabled, timeout?): Promise<void>`**
|
|
951
|
+
|
|
952
|
+
**`getCurrent(timeout?): Promise<UserActionTracker | undefined>`**
|
|
953
|
+
|
|
954
|
+
All async functions follow the same pattern, accepting an optional `timeout` parameter.
|
|
955
|
+
|
|
956
|
+
### UserActionTracker properties
|
|
957
|
+
|
|
958
|
+
| Property | Type | Access | Description |
|
|
959
|
+
|----------|-----------------------------------------------|--------|-------------|
|
|
960
|
+
| `finish()` | Function | - | Completes the user action and sends the event |
|
|
961
|
+
| `subscribe(callback)` | Function | - | Subscribes to automatic completion events |
|
|
962
|
+
| `autoClose` | `boolean` | Get/Set | Controls whether the action closes automatically |
|
|
963
|
+
| `state` | `'active' \| 'complete'` | Get | Returns the current state of the user action |
|
|
964
|
+
| `event_properties` | `Record<string, string \| number \| boolean>` | Get/Set | Custom properties for business context |
|
|
965
|
+
| `startTime` | `number` | Get/Set | Start time in milliseconds (must not be in future) |
|
|
966
|
+
| `name` | `string \| undefined` | Get/Set | Display name for the user action |
|
|
967
|
+
|
|
968
|
+
### Key types and interfaces
|
|
969
|
+
|
|
970
|
+
- `UserActions` - The main interface for user action management
|
|
971
|
+
- `create(options?)` - Creates a new user action
|
|
972
|
+
- `subscribe(subscriber)` - Subscribes to all user action creation events
|
|
973
|
+
- `setAutomaticDetection(enabled)` - Enables or disables automatic detection
|
|
974
|
+
- `current` - Access the currently active user action
|
|
975
|
+
- `UserActionTracker` - Represents an individual user action (see properties table above)
|
|
976
|
+
- `UserActionStartOptions` - Configuration options for creating user actions
|
|
977
|
+
- `autoClose?: boolean` - Whether the action should close automatically (default: `true`)
|
|
978
|
+
- `Unsubscriber` - Function type `() => void` for unsubscribing from events
|
|
979
|
+
|
|
980
|
+
## Additional resources
|
|
981
|
+
|
|
982
|
+
- [RUM JavaScript SDK Overview](../README.md)
|
|
983
|
+
- [Event Modification API](./source/types/api/dynatrace-api-types.ts) - For modifying user action events
|
|
984
|
+
- [RUM JavaScript Typedoc](https://www.dynatrace.com/support/doc/javascriptapi/doc-latest/)
|
|
985
|
+
|
|
986
|
+
---
|
|
987
|
+
|
|
988
|
+
:::note
|
|
989
|
+
This API is experimental and subject to change. Always check for updates when upgrading to new versions of the RUM JavaScript agent.
|
|
990
|
+
:::
|