Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions app/javascript/packages/analytics/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# `@18f/identity-analytics`

Utilities for logging events and errors in the application.

By default, events logged from the frontend will have their names prefixed with "Frontend:". This
behavior occurs in [`FrontendLogController`][frontend_log_controller.rb]. You can avoid the prefix
by assigning an event mapping method in the controller's `EVENT_MAP` constant.

[frontend_log_controller.rb]: https://github.com/18F/identity-idp/blob/main/app/controllers/frontend_log_controller.rb

## Example

```ts
import { trackEvent, trackError } from '@18f/identity-analytics';

button.addEventListener('click', () => {
trackEvent('Button clicked', { success: true });
});

try {
doSomethingRisky();
} catch (error) {
trackError(error);
}
```
Original file line number Diff line number Diff line change
Expand Up @@ -278,7 +278,7 @@ function AcuantCapture(
sharpnessThreshold,
} = useContext(AcuantContext);
const { isMockClient } = useContext(UploadContext);
const { addPageAction } = useContext(AnalyticsContext);
const { trackEvent } = useContext(AnalyticsContext);
const fullScreenRef = useRef<FullScreenRefHandle>(null);
const inputRef = useRef<HTMLInputElement>(null);
const isForceUploading = useRef(false);
Expand Down Expand Up @@ -351,7 +351,7 @@ function AcuantCapture(
size: nextValue.size,
});

addPageAction(`IdV: ${name} image added`, analyticsPayload);
trackEvent(`IdV: ${name} image added`, analyticsPayload);
}

onChangeAndResetError(nextValue, analyticsPayload);
Expand All @@ -365,7 +365,7 @@ function AcuantCapture(
return <T extends (...args: any[]) => any>(fn: T) =>
(...args: Parameters<T>) => {
if (!isSuppressingClickLogging.current) {
addPageAction(`IdV: ${name} image clicked`, { source, ...metadata });
trackEvent(`IdV: ${name} image clicked`, { source, ...metadata });
}

return fn(...args);
Expand Down Expand Up @@ -415,7 +415,7 @@ function AcuantCapture(
function startCaptureOrTriggerUpload(event: MouseEvent) {
if (event.target === inputRef.current) {
if (forceNativeCamera) {
addPageAction('IdV: Native camera forced after failed attempts', {
trackEvent('IdV: Native camera forced after failed attempts', {
field: name,
failed_attempts: failedCaptureAttempts,
});
Expand Down Expand Up @@ -483,7 +483,7 @@ function AcuantCapture(
size: getDecodedBase64ByteSize(nextCapture.image.data),
});

addPageAction(`IdV: ${name} image added`, analyticsPayload);
trackEvent(`IdV: ${name} image added`, analyticsPayload);

if (assessment === 'success') {
onChangeAndResetError(data, analyticsPayload);
Expand Down Expand Up @@ -528,7 +528,7 @@ function AcuantCapture(
}

setIsCapturingEnvironment(false);
addPageAction('IdV: Image capture failed', {
trackEvent('IdV: Image capture failed', {
field: name,
error: getNormalizedAcuantCaptureFailureMessage(error, code),
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import CaptureAdvice from './capture-advice';
* @param {CaptureTroubleshootingProps} props
*/
function CaptureTroubleshooting({ children }) {
const { addPageAction } = useContext(AnalyticsContext);
const { trackEvent } = useContext(AnalyticsContext);
const [didShowTroubleshooting, setDidShowTroubleshooting] = useState(false);
const { failedCaptureAttempts, maxFailedAttemptsBeforeTips, lastAttemptMetadata } = useContext(
FailedCaptureAttemptsContext,
Expand All @@ -28,13 +28,13 @@ function CaptureTroubleshooting({ children }) {
const { isAssessedAsGlare, isAssessedAsBlurry } = lastAttemptMetadata;

function onCaptureTipsShown() {
addPageAction('IdV: Capture troubleshooting shown', lastAttemptMetadata);
trackEvent('IdV: Capture troubleshooting shown', lastAttemptMetadata);

onPageTransition();
}

function onCaptureTipsDismissed() {
addPageAction('IdV: Capture troubleshooting dismissed');
trackEvent('IdV: Capture troubleshooting dismissed');

setDidShowTroubleshooting(true);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -79,14 +79,14 @@ function ReviewIssuesStep({
const { t } = useI18n();
const { isMobile } = useContext(DeviceContext);
const serviceProvider = useContext(ServiceProviderContext);
const { addPageAction } = useContext(AnalyticsContext);
const { trackEvent } = useContext(AnalyticsContext);
const selfieError = errors.find(({ field }) => field === 'selfie')?.error;
const [hasDismissed, setHasDismissed] = useState(remainingAttempts === Infinity);
const { onPageTransition, changeStepCanComplete } = useContext(FormStepsContext);
useDidUpdateEffect(onPageTransition, [hasDismissed]);

function onWarningPageDismissed() {
addPageAction('IdV: Capture troubleshooting dismissed');
trackEvent('IdV: Capture troubleshooting dismissed');

setHasDismissed(true);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,9 +50,9 @@ function Warning({
location,
remainingAttempts,
}: WarningProps) {
const { addPageAction } = useContext(AnalyticsContext);
const { trackEvent } = useContext(AnalyticsContext);
useEffect(() => {
addPageAction('IdV: warning shown', { location, remaining_attempts: remainingAttempts });
trackEvent('IdV: warning shown', { location, remaining_attempts: remainingAttempts });
}, []);

let actionButtons: ReactComponentElement<typeof Button>[] | undefined;
Expand All @@ -62,7 +62,7 @@ function Warning({
isBig
isWide
onClick={() => {
addPageAction('IdV: warning action triggered', { location });
trackEvent('IdV: warning action triggered', { location });
actionOnClick();
}}
>
Expand Down
6 changes: 3 additions & 3 deletions app/javascript/packages/document-capture/context/acuant.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -214,7 +214,7 @@ function AcuantContextProvider({
children,
}: AcuantContextProviderProps) {
const { isMobile } = useContext(DeviceContext);
const { addPageAction } = useContext(AnalyticsContext);
const { trackEvent } = useContext(AnalyticsContext);
// Only mobile devices should load the Acuant SDK. Consider immediately ready otherwise.
const [isReady, setIsReady] = useState(!isMobile);
const [isAcuantLoaded, setIsAcuantLoaded] = useState(false);
Expand Down Expand Up @@ -262,7 +262,7 @@ function AcuantContextProvider({
window.AcuantJavascriptWebSdk.startWorkers(() => {
window.AcuantCamera = getActualAcuantCamera();
const { isCameraSupported: nextIsCameraSupported } = window.AcuantCamera;
addPageAction('IdV: Acuant SDK loaded', {
trackEvent('IdV: Acuant SDK loaded', {
success: true,
isCameraSupported: nextIsCameraSupported,
});
Expand All @@ -273,7 +273,7 @@ function AcuantContextProvider({
});
},
onFail(code, description) {
addPageAction('IdV: Acuant SDK loaded', {
trackEvent('IdV: Acuant SDK loaded', {
success: false,
code,
description,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,12 @@ import { createContext } from 'react';
/**
* @typedef AnalyticsContext
*
* @prop {TrackEvent} addPageAction Log an action with optional payload.
* @prop {TrackEvent} trackEvent Log an action with optional payload.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should we link to the controller here with the list of payload names it recognizes?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Technically the controller will handle any event we throw at it, and the allowlist only exists to avoid the "Frontend:" prefixing. But I think it wouldn't hurt to include a reference to it.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually, since this context is specific to document capture, it'd probably be more applicable in the @18f/identity-analytics package, and it might be something we could surface in a README.md there, which would be good to have anyways. I think I'll go that route.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it might be something we could surface in a README.md there, which would be good to have anyways. I think I'll go that route.

Added in be566e7.

*/

const AnalyticsContext = createContext(
/** @type {AnalyticsContext} */ ({
addPageAction: () => Promise.resolve(),
trackEvent: () => Promise.resolve(),
}),
);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ const withBackgroundEncryptedUpload = (Component) => {
*/
function ComposedComponent({ onChange, onError, ...props }) {
const { backgroundUploadURLs, backgroundUploadEncryptKey } = useContext(UploadContext);
const { addPageAction } = useContext(AnalyticsContext);
const { trackEvent } = useContext(AnalyticsContext);

/**
* @param {Record<string, string|Blob|null|undefined>} nextValues Next values.
Expand All @@ -103,14 +103,14 @@ const withBackgroundEncryptedUpload = (Component) => {
value,
)
.catch((error) => {
addPageAction('IdV: document capture async upload encryption', { success: false });
trackEvent('IdV: document capture async upload encryption', { success: false });
trackError(error);

// Rethrow error to skip upload and proceed from next `catch` block.
throw error;
})
.then((encryptedValue) => {
addPageAction('IdV: document capture async upload encryption', { success: true });
trackEvent('IdV: document capture async upload encryption', { success: true });

return window.fetch(url, {
method: 'PUT',
Expand All @@ -120,7 +120,7 @@ const withBackgroundEncryptedUpload = (Component) => {
})
.then((response) => {
const traceId = response.headers.get('X-Amzn-Trace-Id');
addPageAction('IdV: document capture async upload submitted', {
trackEvent('IdV: document capture async upload submitted', {
success: response.ok,
trace_id: traceId,
status_code: response.status,
Expand Down
8 changes: 4 additions & 4 deletions app/javascript/packs/document-capture.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import {
} from '@18f/identity-document-capture';
import { isCameraCapableMobile } from '@18f/identity-device';
import { FlowContext } from '@18f/identity-verify-flow';
import { trackEvent } from '@18f/identity-analytics';
import { trackEvent as baseTrackEvent } from '@18f/identity-analytics';

/** @typedef {import('@18f/identity-document-capture').FlowPath} FlowPath */
/** @typedef {import('@18f/identity-i18n').I18n} I18n */
Expand Down Expand Up @@ -93,9 +93,9 @@ const device = {
};

/** @type {import('@18f/identity-analytics').trackEvent} */
function addPageAction(event, payload) {
function trackEvent(event, payload) {
const { flowPath } = appRoot.dataset;
return trackEvent(event, { ...payload, flow_path: flowPath });
return baseTrackEvent(event, { ...payload, flow_path: flowPath });
}

(async () => {
Expand Down Expand Up @@ -145,7 +145,7 @@ function addPageAction(event, payload) {
[AppContext.Provider, { value: { appName } }],
[MarketingSiteContextProvider, { helpCenterRedirectURL, securityAndPrivacyHowItWorksURL }],
[DeviceContext.Provider, { value: device }],
[AnalyticsContext.Provider, { value: { addPageAction } }],
[AnalyticsContext.Provider, { value: { trackEvent } }],
[
AcuantContextProvider,
{
Expand Down
Loading