Skip to content

Analytics ​

Overview ​

  1. Amplitude for behavioral tracking: User interactions are tracked via Amplitude. The SDK is initialized in plugins/amplitude.client.ts, which provides a $amplitude tracker object to the Nuxt app. Components never call amplitude.track() directly — they use the composables useAmplitude, useAmplitudeAssetTracking, or useAmplitudeSearchTracking.
  2. 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.
  3. Context enrichment plugin: Every Amplitude event is automatically enriched with workspace_id, workspace_name, page_name, page_path, origin_url, and environment via a custom SDK enrichment plugin registered at init time.
  4. 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.
  5. 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.
  6. Custom SPA referrer tracking: Amplitude's built-in referrer is frozen at page load. The plugin tracks previousFullUrl across client-side navigations via router.afterEach so events report the actual last in-app page, not the original document referrer.
  7. ID normalization: utils/analyticsIds.ts exports normalizeIdProperties which 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, $amplitude provider

TypeScript Composable Files ​

  • app/composables/core/common/useAmplitude.ts — core tracking composable: trackActivity, trackViewModeChange, trackVisibilityChange, selection method helpers
  • app/composables/core/common/useAmplitudeAssetTracking.ts — asset-specific tracking: image editor events, custom field change events
  • app/composables/core/common/useAmplitudeSearchTracking.ts — search tracking: trackSearchPerformed (with deduplication), trackSearchResultClick, trackFilterChange

TypeScript Utility Files ​

  • app/utils/amplitudeSessionReplay.ts — initSessionReplay, shutdownSessionReplay, setSessionReplaySessionId, getSessionReplayPrivacyConfig
  • app/utils/analyticsIds.ts — normalizeIdProperties, ID_KEYS, MULTI_KEYS, COUNT_KEYS, SINGLE_VALUED, isSingleValued

TypeScript Constant Files ​

  • app/constants/amplitudeEvents.ts — AMPLITUDE_EVENTS object (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 isInitialized guard — calling initAmplitude() 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
  • pageUrlEnrichment disabled (the enrichment plugin handles this)
  • Session timeout: 30 minutes
  • immediate flush 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 ​

SettingValue
Default mask level'medium'
Masked selectorspassword/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]
StorageIndexedDB (storeType: 'idb')
Web Workerenabled
Performance timeout2000 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) ​

ConstantEvent string
ASSET_ADDEDasset added
ASSET_ARCHIVEDasset archived
ASSET_CONVERTEDasset converted
ASSET_DELETEDasset deleted
ASSET_DOWNLOADEDasset downloaded
ASSET_DUPLICATEDasset duplicated
ASSET_EMBEDDEDasset embedded
ASSET_MOVEDasset moved
ASSET_TAGS_ADDEDasset tags-added
ASSET_TAGS_REMOVEDasset tags-removed
ASSET_UPDATEasset update
ASSET_VIEWEDasset viewed
ASSET_VISIBILITY_UPDATEDasset visibility-updated
ASSET_VERSION_ADDEDasset version-added
ASSET_VERSION_DELETEDasset version-deleted
ASSET_VERSION_DOWNLOADEDasset version-downloaded
ASSET_VERSION_PREVIEWEDasset version-previewed
ASSET_VERSION_RESTOREDasset version-restored

Asset Editor Events (8) ​

ConstantEvent string
ASSET_EDITOR_OPENEDasset editor-opened
ASSET_EDITOR_RESIZEDasset editor-resized
ASSET_EDITOR_CROPPEDasset editor-cropped
ASSET_EDITOR_ROTATEDasset editor-rotated
ASSET_EDITOR_FLIPPEDasset editor-flipped
ASSET_EDITOR_FORMAT_CHANGEDasset editor-format-changed
ASSET_EDITOR_RESETasset editor-reset
ASSET_EDITOR_DOWNLOADEDasset 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_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 track

User 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 identity

Component 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>
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>