Appearance
Analytics ​
Overview ​
- Amplitude for behavioral tracking: User interactions are tracked via Amplitude. The SDK is initialized in
plugins/amplitude.client.ts, which provides a$amplitudetracker object to the Nuxt app. Components never callamplitude.track()directly — they use the composablesuseAmplitude,useAmplitudeAssetTracking, oruseAmplitudeSearchTracking. - 124 named events: All event names are defined as string constants in
constants/amplitudeEvents.ts(AMPLITUDE_EVENTS). This is the single source of truth — do not hardcode event name strings in components. - Context enrichment plugin: Every Amplitude event is automatically enriched with
workspace_id,workspace_name,page_name,page_path,origin_url, andenvironmentvia a custom SDK enrichment plugin registered at init time. - PII protection: User email and name are SHA-256 hashed before being sent to Amplitude. The raw values never leave the browser as user properties.
- Session Replay: Amplitude Session Replay is initialized when
NUXT_PUBLIC_AMPLITUDE_SESSION_REPLAY_ENABLED=true(or a non-zero sample rate is provided). It uses a medium default mask level and blocks sensitive input selectors. Components can opt out of recording with the[data-amp-block]or[data-no-replay]attribute. - Custom SPA referrer tracking: Amplitude's built-in referrer is frozen at page load. The plugin tracks
previousFullUrlacross client-side navigations viarouter.afterEachso events report the actual last in-app page, not the original document referrer. - ID normalization:
utils/analyticsIds.tsexportsnormalizeIdPropertieswhich coerces IDs to strings, converts multi-value ID fields to arrays, and auto-derives count properties (asset_count,category_count) from arrays.
Architecture ​
The amplitude plugin initializes the Amplitude SDK once (isInitialized guard), registers the context-enrichment plugin, starts session replay (after obtaining a device ID), and sets up a router.afterEach hook for SPA referrer tracking. It watches the user ref from useAuth — on login it calls identifyFromUser to set user properties; on logout it calls reset() and shuts down session replay. The tracker object (AmplitudeTracker) is provided as $amplitude to the Nuxt app. The three tracking composables retrieve $amplitude via useNuxtApp() and expose semantic tracking functions that components call. Event properties flow through sanitizeEventProperties before dispatch, which recursively strips functions, symbols, NaN, Infinity, and truncates strings exceeding 1000 characters.
File Structure ​
TypeScript Plugin Files ​
app/plugins/amplitude.client.ts— SDK initialization, context enrichment, session replay bootstrap, PII hashing,$amplitudeprovider
TypeScript Composable Files ​
app/composables/core/common/useAmplitude.ts— core tracking composable:trackActivity,trackViewModeChange,trackVisibilityChange, selection method helpersapp/composables/core/common/useAmplitudeAssetTracking.ts— asset-specific tracking: image editor events, custom field change eventsapp/composables/core/common/useAmplitudeSearchTracking.ts— search tracking:trackSearchPerformed(with deduplication),trackSearchResultClick,trackFilterChange
TypeScript Utility Files ​
app/utils/amplitudeSessionReplay.ts—initSessionReplay,shutdownSessionReplay,setSessionReplaySessionId,getSessionReplayPrivacyConfigapp/utils/analyticsIds.ts—normalizeIdProperties,ID_KEYS,MULTI_KEYS,COUNT_KEYS,SINGLE_VALUED,isSingleValued
TypeScript Constant Files ​
app/constants/amplitudeEvents.ts—AMPLITUDE_EVENTSobject (124 constants),AMPLITUDE_ACTIVITY_KEYS,AMPLITUDE_COMMON_ACTIVITY_KEYS,AMPLITUDE_MAX_FIELD_VALUE_LENGTH
TypeScript Type Files ​
app/types/amplitude.ts—AmplitudeTracker,AmplitudeEventProperties,AmplitudeUserProperties,AmplitudeIdentifiableUser,SessionReplayInitOptions,SessionReplayPrivacyConfig,AmplitudeEnvironment,AmplitudeTrackOptions
amplitude.client.ts Plugin ​
File: app/plugins/amplitude.client.ts
Features ​
- Single initialization with
isInitializedguard — callinginitAmplitude()twice is a no-op - Diagnostics disabled (
enableDiagnostics: false,_setDiagnosticsSampleRate(0)) - Autocapture: page views, sessions, attribution, element interactions on allowlisted selectors, frustration interactions, Web Vitals
- Network tracking captures non-2xx responses from the app domain and API host
- Form interactions and file download autocapture are explicitly disabled
pageUrlEnrichmentdisabled (the enrichment plugin handles this)- Session timeout: 30 minutes
immediateflush option for events fired before redirect or logout
AmplitudeTracker Interface ​
typescript
interface AmplitudeTracker {
track(eventName: string, eventProperties?: AmplitudeEventProperties, options?: AmplitudeTrackOptions): void
identify(userId: string, userProperties?: AmplitudeUserProperties): Promise<void>
identifyFromUser(source: AmplitudeIdentifiableUser): void
setUserProperties(properties: AmplitudeUserProperties): void
reset(): Promise<void>
flush(): void
getSessionId(): number | null
getDeviceId(): string | null
syncSessionReplaySessionId(): void
}Context Enrichment Properties (added to every event) ​
typescript
{
workspace_id: string | number | null,
workspace_name: string | null,
page_name: string | null, // Nuxt route name
page_path: string | null, // current path
origin_url: string | null, // window.location.origin
environment: 'admin' | 'anonymous',
referrer: string | null, // previous in-app URL
referring_domain: string | null, // host of referrer
}Session Replay Configuration ​
| Setting | Value |
|---|---|
| Default mask level | 'medium' |
| Masked selectors | password/email inputs, credit card fields, .amp-mask, [data-amp-mask] |
| Blocked selectors | .amp-block, [data-amp-block], [data-no-replay] |
| Unmasked selectors | .amp-unmask, [data-amp-unmask] |
| Storage | IndexedDB (storeType: 'idb') |
| Web Worker | enabled |
| Performance timeout | 2000 ms |
useAmplitude Composable ​
File: app/composables/core/common/useAmplitude.ts
Return Value ​
typescript
{
trackActivity(payload: ActivityPayload | null | undefined, extraProperties?: AmplitudeEventProperties): void
recordSelectionMethod(method: string | null): void
selectionMethodFor(count: number): AmplitudeEventProperties
changedFields(pairs: ChangedFieldPairs): string[]
cascadeSize(items: unknown): AmplitudeEventProperties
syncViewMode(mode: string): void
trackViewModeChange(mode: string, previous?: string | null, identifier?: string | null): void
trackVisibilityChange(activity: ActivityPayload, before: Array<...>, after: Array<...>, extra?: AmplitudeEventProperties): void
}trackActivity ​
The primary method for tracking most user actions. It takes an ActivityPayload whose event and sub_event fields form the Amplitude event name ("{event} {sub_event}"). The composable picks only the allowed property keys for that event noun from AMPLITUDE_ACTIVITY_KEYS, adds common keys, normalizes IDs, and tracks.
typescript
// In a component
const { trackActivity } = useAmplitude()
// Track asset viewed
trackActivity({
event: 'asset',
sub_event: 'viewed',
asset_id: ['123'],
category_id: ['456'],
})
// Track folder deleted with bulk cascade size
const { trackActivity, cascadeSize } = useAmplitude()
trackActivity(
{ event: 'folder', sub_event: 'deleted', category_id: ['789'] },
{ ...cascadeSize(deletedFolders) }
)trackViewModeChange ​
Tracks when a user switches between grid and list view. Also updates the view_mode user property in Amplitude so the preference is visible in cohort analysis.
typescript
const { trackViewModeChange } = useAmplitude()
trackViewModeChange('grid', 'list', 'dam-folders')
// Fires: 'user view-mode-changed' { from_mode: 'list', to_mode: 'grid', scope: 'dam-folders' }useAmplitudeAssetTracking Composable ​
File: app/composables/core/common/useAmplitudeAssetTracking.ts
Return Value ​
typescript
{
trackImageEditor(verb: string, properties?: AmplitudeEventProperties): void
trackCustomFieldsUpdated(
assetIds: Array<number | string> | number | string,
fields: TrackableCustomField[],
isMultiple?: boolean,
forcedKind?: CustomFieldChangeKind | null
): void
}trackImageEditor ​
Constructs the event name as asset editor-{verb} and fires it. Used for crop, rotate, flip, resize, format-change, reset, and download actions inside the image editor.
typescript
const { trackImageEditor } = useAmplitudeAssetTracking()
trackImageEditor('cropped', { asset_id: '123' })
// Fires: 'asset editor-cropped' { asset_id: '123' }trackCustomFieldsUpdated ​
Groups field changes by kind (added, updated, removed) and fires one event per kind. Values are truncated to AMPLITUDE_MAX_FIELD_VALUE_LENGTH (100 chars) and labelled as FieldLabel=value.
useAmplitudeSearchTracking Composable ​
File: app/composables/core/common/useAmplitudeSearchTracking.ts
Return Value ​
typescript
{
trackSearchPerformed(payload: SearchPerformedPayload): void
trackSearchResultClick(resultType: SearchResultType, item: SearchResultEntry | null, list: SearchResultEntry[] | null): void
trackFilterChange(nextList: SearchFilterEntry[] | null): void
}Deduplication ​
trackSearchPerformed deduplicates: if the same query + filter types + active tab combination fires within 1000 ms, the second call is silently dropped.
trackSearchResultClick ​
Computes the 1-based rank of the clicked result within the results array and fires search result-clicked with result_type, rank, and results_loaded.
trackFilterChange ​
Diffs the previous filter list against the new one by counting occurrences of each level1 type. Fires search filter-applied once per type that changed, with action: 'add' or action: 'remove'.
Event Catalog ​
All events are defined in constants/amplitudeEvents.ts as AMPLITUDE_EVENTS:
Asset Events (18) ​
| Constant | Event string |
|---|---|
ASSET_ADDED | asset added |
ASSET_ARCHIVED | asset archived |
ASSET_CONVERTED | asset converted |
ASSET_DELETED | asset deleted |
ASSET_DOWNLOADED | asset downloaded |
ASSET_DUPLICATED | asset duplicated |
ASSET_EMBEDDED | asset embedded |
ASSET_MOVED | asset moved |
ASSET_TAGS_ADDED | asset tags-added |
ASSET_TAGS_REMOVED | asset tags-removed |
ASSET_UPDATE | asset update |
ASSET_VIEWED | asset viewed |
ASSET_VISIBILITY_UPDATED | asset visibility-updated |
ASSET_VERSION_ADDED | asset version-added |
ASSET_VERSION_DELETED | asset version-deleted |
ASSET_VERSION_DOWNLOADED | asset version-downloaded |
ASSET_VERSION_PREVIEWED | asset version-previewed |
ASSET_VERSION_RESTORED | asset version-restored |
Asset Editor Events (8) ​
| Constant | Event string |
|---|---|
ASSET_EDITOR_OPENED | asset editor-opened |
ASSET_EDITOR_RESIZED | asset editor-resized |
ASSET_EDITOR_CROPPED | asset editor-cropped |
ASSET_EDITOR_ROTATED | asset editor-rotated |
ASSET_EDITOR_FLIPPED | asset editor-flipped |
ASSET_EDITOR_FORMAT_CHANGED | asset editor-format-changed |
ASSET_EDITOR_RESET | asset editor-reset |
ASSET_EDITOR_DOWNLOADED | asset editor-downloaded |
Asset Custom Field Events (3) ​
ASSET_CUSTOM_FIELDS_ADDED, ASSET_CUSTOM_FIELDS_UPDATED, ASSET_CUSTOM_FIELDS_REMOVED
Collage Events (9) ​
COLLAGE_ASSET_ADDED, COLLAGE_ASSET_REMOVED, COLLAGE_CREATED, COLLAGE_DELETED, COLLAGE_DOWNLOADED, COLLAGE_SHARED, COLLAGE_UPDATE, COLLAGE_VIEWED, COLLAGE_VISIBILITY_UPDATED
Folder Events (8) ​
FOLDER_CREATED, FOLDER_DELETED, FOLDER_DOWNLOADED, FOLDER_MOVED, FOLDER_SHARED, FOLDER_UPDATE, FOLDER_VIEWED, FOLDER_VISIBILITY_UPDATED
Shared Link Events (6) ​
SHARED_DOWNLOADED, SHARED_SHARED, SHARED_VIEWED, SHARED_LINK_UPDATED, SHARED_PASSWORD_FAILED, SHARED_PASSWORD_VERIFIED
User / Auth Events (21) ​
USER_ACCESS_REQUESTED, USER_ACCESS_REQUEST_FAILED, USER_INVITATION_ACCEPTED, USER_INVITATION_ACCEPT_FAILED, USER_LOGGED_IN, USER_LOGGED_OUT, USER_LOGIN_FAILED, USER_NOTIFICATION_PREF_UPDATED, USER_PASSKEY_ADDED, USER_PASSKEY_ADD_FAILED, USER_PASSKEY_REMOVED, USER_PASSWORD_CHANGED, USER_PASSWORD_RESET_REQUESTED, USER_PASSWORD_RESET_REQUEST_FAILED, USER_PASSWORD_SET, USER_PROFILE_IMAGE_UPDATED, USER_PROFILE_UPDATED, USER_SUPPORT_PIN_GENERATED, USER_SUPPORT_SESSION_TERMINATED, USER_SUPPORT_SESSIONS_CLEARED, USER_VIEW_MODE_CHANGED
Workspace Events (14) ​
WORKSPACE_CREATED, WORKSPACE_CUSTOM_FIELD_DELETED, WORKSPACE_CUSTOM_FIELD_TOGGLED, WORKSPACE_DOMAIN_UPDATED, WORKSPACE_DOMAIN_VERIFIED, WORKSPACE_DOMAIN_VERIFY_FAILED, WORKSPACE_FAVICON_UPDATED, WORKSPACE_INSTANCE_SETTINGS_UPDATED, WORKSPACE_LOGO_UPDATED, WORKSPACE_NOTIFICATION_PREFS_UPDATED, WORKSPACE_NOTIFICATIONS_TOGGLED, WORKSPACE_OWNER_CHANGED, WORKSPACE_SETTINGS_UPDATED, WORKSPACE_SWITCHED
Member Events (7) ​
MEMBER_ACTIVATED, MEMBER_ADDED, MEMBER_DEACTIVATED, MEMBER_INVITED, MEMBER_INVITE_RESENT, MEMBER_PASSWORD_RESET, MEMBER_UPDATED
Portal Events (19) ​
PORTAL_BANNER_DELETED, PORTAL_BANNER_TOGGLED, PORTAL_BANNERS_REORDERED, PORTAL_BRANDING_UPDATED, PORTAL_CREATED, PORTAL_FAVICON_UPDATED, PORTAL_INVITE_DELETED, PORTAL_INVITE_RESENT, PORTAL_LOGO_UPDATED, PORTAL_SETTINGS_UPDATED, PORTAL_SLIDER_TOGGLED, PORTAL_THEME_UPDATED, PORTAL_TILE_DELETED, PORTAL_TILE_TOGGLED, PORTAL_TILES_REORDERED, PORTAL_USER_ACTIVATED, PORTAL_USER_DEACTIVATED, PORTAL_USER_PASSWORD_RESET, PORTAL_VISIBILITY_CHANGED
Search Events (3) ​
SEARCH_PERFORMED, SEARCH_RESULT_CLICKED, SEARCH_FILTER_APPLIED
Trash Events (2) ​
TRASH_RESTORED, TRASH_PERMANENTLY_DELETED
Miscellaneous Events (6) ​
FEEDBACK_SUBMITTED, GUEST_URL_COPIED, ASSET_EMBED_REMOVED, ASSET_UPLOAD_FAILED, SUBSCRIPTION_CANCELED, SUBSCRIPTION_TRIAL_CANCELED
Workflows ​
Event Tracking Flow ​
1. User performs action in component
e.g. downloads an asset
↓
2. Component calls tracking composable
const { trackActivity } = useAmplitude()
trackActivity({ event: 'asset', sub_event: 'downloaded', asset_id: ['123'] })
↓
3. trackActivity builds event name
'asset downloaded'
↓
4. activityProperties filters allowed keys
AMPLITUDE_ACTIVITY_KEYS['asset'] = ['asset_id', 'category_id', ...]
Only allowed keys pass through
↓
5. normalizeIdProperties runs on properties
asset_id string → coerced to string
asset_id array → array of strings + asset_count derived
↓
6. $amplitude.track() called
SDK queues the event
↓
7. Context enrichment plugin runs (SDK enrichment phase)
Adds workspace_id, workspace_name, page_name, page_path, origin_url,
environment, referrer, referring_domain
↓
8. sanitizeEventProperties runs inside tracker.track()
Strips NaN, Infinity, functions, symbols
Truncates strings > 1000 chars
↓
9. Amplitude SDK batches and flushes
Batch size: 10 events or 5 second timeout
Immediate events (logged-in, login-failed) call amplitude.flush() after trackUser Identification Flow ​
1. User logs in successfully
auth-hydrate.global.ts calls amplitude plugin
↓
2. identifyFromUser(user) called
Builds AmplitudeUserProperties from user object:
email, name, user_type, subscription_user, workspace details, trial status
↓
3. PII hashing
email → SHA-256 hash → stored as email_hash (email removed)
name → SHA-256 hash → stored as name_hash (name removed)
↓
4. amplitude.Identify() built and sent
All properties set via identity.set()
amplitude.identify(identity) called
↓
5. Session replay initializes
initSessionReplay({ apiKey, sampleRate, deviceId, sessionId })
Waits for deviceId and sessionId to be available (up to 10 retries × 100ms)
↓
6. User logs out
tracker.reset() called
shutdownSessionReplay() called (flushes replay buffer)
amplitude.reset() regenerates device ID and clears user identityComponent Integration ​
Tracking a User Action ​
vue
<script setup lang="ts">
const { trackActivity, cascadeSize } = useAmplitude()
async function deleteFolder(folderId: number) {
await useFolderApi().deleteFolder({ folder_id: folderId })
trackActivity(
{
event: 'folder',
sub_event: 'deleted',
category_id: [String(folderId)],
}
)
}
async function deleteSelectedFolders(folders: Folder[]) {
// bulk delete — pass cascadeSize to record downstream impact
const folderIds = folders.map((f) => String(f.id))
trackActivity(
{
event: 'folder',
sub_event: 'deleted',
category_id: folderIds,
},
cascadeSize(folders)
)
}
</script>Tracking Search ​
vue
<script setup lang="ts">
const { trackSearchPerformed, trackSearchResultClick, trackFilterChange } =
useAmplitudeSearchTracking()
// Fire after results load
watch(searchResults, (results) => {
trackSearchPerformed({
query: searchQuery.value,
filterTypes: activeFilters.value.map((f) => f.level1 ?? ''),
filterCount: activeFilters.value.length,
activeTab: currentTab.value,
counts: {
assets: results.assets?.total ?? null,
folders: results.folders?.total ?? null,
collages: results.collages?.total ?? null,
},
trigger: 'search',
})
})
// Fire on result click
function onResultClick(resultType: SearchResultType, item: SearchResultEntry) {
trackSearchResultClick(resultType, item, allResults.value)
}
// Fire on filter change
watch(activeFilters, (next) => trackFilterChange(next))
</script>Blocking Session Replay on Sensitive Elements ​
html
<!-- Entire panel excluded from recording -->
<div data-no-replay>
<SensitiveDataPanel />
</div>
<!-- Single element masked (blurred in replay) -->
<input class="amp-mask" type="text" />
<!-- Element included even inside a masked parent -->
<span class="amp-unmask">{{ publicLabel }}</span>