Skip to content

Commit e13f7a6

Browse files
[FSSDK-9871] Hook for track events (#268)
* [FSSDK-9871] Hook for track events * [FSSDK-9871] useTrackEvents test addition * [FSSDK-9871] useTrackEvents hook + test improvement * [FSSDK-9871] readme update * [FSSDK-9871] readme fix * [FSSDK-9871] singular hook name and interface update + copyright update
1 parent 8c937da commit e13f7a6

File tree

4 files changed

+132
-11
lines changed

4 files changed

+132
-11
lines changed

README.md

+20-1
Original file line numberDiff line numberDiff line change
@@ -328,8 +328,27 @@ function MyComponent() {
328328
```
329329

330330
### Tracking
331+
Use the built-in `useTrackEvent` hook to access the `track` method of optimizely instance
331332

332-
Use the `withOptimizely` HoC for tracking.
333+
```jsx
334+
import { useTrackEvent } from '@optimizely/react-sdk';
335+
336+
function SignupButton() {
337+
const [track, clientReady, didTimeout] = useTrackEvent()
338+
339+
const handleClick = () => {
340+
if(clientReady) {
341+
track('signup-clicked')
342+
}
343+
}
344+
345+
return (
346+
<button onClick={handleClick}>Signup</button>
347+
)
348+
}
349+
```
350+
351+
Or you can use the `withOptimizely` HoC.
333352

334353
```jsx
335354
import { withOptimizely } from '@optimizely/react-sdk';

src/hooks.spec.tsx

+56-7
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/**
2-
* Copyright 2022, 2023 Optimizely
2+
* Copyright 2022, 2023, 2024 Optimizely
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -18,14 +18,13 @@
1818

1919
import * as React from 'react';
2020
import { act } from 'react-dom/test-utils';
21-
import { render, screen, waitFor } from '@testing-library/react';
21+
import { render, renderHook, screen, waitFor } from '@testing-library/react';
2222
import '@testing-library/jest-dom/extend-expect';
2323

2424
import { OptimizelyProvider } from './Provider';
2525
import { OnReadyResult, ReactSDKClient, VariableValuesObject } from './client';
26-
import { useExperiment, useFeature, useDecision } from './hooks';
26+
import { useExperiment, useFeature, useDecision, useTrackEvent, hooksLogger } from './hooks';
2727
import { OptimizelyDecision } from './utils';
28-
2928
const defaultDecision: OptimizelyDecision = {
3029
enabled: false,
3130
variables: {},
@@ -80,7 +79,7 @@ describe('hooks', () => {
8079
let forcedVariationUpdateCallbacks: Array<() => void>;
8180
let decideMock: jest.Mock<OptimizelyDecision>;
8281
let setForcedDecisionMock: jest.Mock<void>;
83-
82+
let hooksLoggerErrorSpy: jest.SpyInstance;
8483
const REJECTION_REASON = 'A rejection reason you should never see in the test runner';
8584

8685
beforeEach(() => {
@@ -99,7 +98,6 @@ describe('hooks', () => {
9998
);
10099
}, timeout || mockDelay);
101100
});
102-
103101
activateMock = jest.fn();
104102
isFeatureEnabledMock = jest.fn();
105103
featureVariables = mockFeatureVariables;
@@ -110,7 +108,7 @@ describe('hooks', () => {
110108
forcedVariationUpdateCallbacks = [];
111109
decideMock = jest.fn();
112110
setForcedDecisionMock = jest.fn();
113-
111+
hooksLoggerErrorSpy = jest.spyOn(hooksLogger, 'error');
114112
optimizelyMock = ({
115113
activate: activateMock,
116114
onReady: jest.fn().mockImplementation(config => getOnReadyPromise(config || {})),
@@ -141,6 +139,7 @@ describe('hooks', () => {
141139
getForcedVariations: jest.fn().mockReturnValue({}),
142140
decide: decideMock,
143141
setForcedDecision: setForcedDecisionMock,
142+
track: jest.fn(),
144143
} as unknown) as ReactSDKClient;
145144

146145
mockLog = jest.fn();
@@ -168,6 +167,7 @@ describe('hooks', () => {
168167
res => res.dataReadyPromise,
169168
err => null
170169
);
170+
hooksLoggerErrorSpy.mockReset();
171171
});
172172

173173
describe('useExperiment', () => {
@@ -1048,4 +1048,53 @@ describe('hooks', () => {
10481048
await waitFor(() => expect(screen.getByTestId('result')).toHaveTextContent('false|{}|true|false'));
10491049
});
10501050
});
1051+
describe('useTrackEvent', () => {
1052+
it('returns track method and false states when optimizely is not provided', () => {
1053+
const wrapper = ({ children }: { children: React.ReactNode }): React.ReactElement => {
1054+
//@ts-ignore
1055+
return <OptimizelyProvider>{children}</OptimizelyProvider>;
1056+
};
1057+
const { result } = renderHook(() => useTrackEvent(), { wrapper });
1058+
expect(result.current[0]).toBeInstanceOf(Function);
1059+
expect(result.current[1]).toBe(false);
1060+
expect(result.current[2]).toBe(false);
1061+
});
1062+
1063+
it('returns track method along with clientReady and didTimeout state when optimizely instance is provided', () => {
1064+
const wrapper = ({ children }: { children: React.ReactNode }): React.ReactElement => (
1065+
<OptimizelyProvider optimizely={optimizelyMock} isServerSide={false} timeout={10000}>
1066+
{children}
1067+
</OptimizelyProvider>
1068+
);
1069+
1070+
const { result } = renderHook(() => useTrackEvent(), { wrapper });
1071+
expect(result.current[0]).toBeInstanceOf(Function);
1072+
expect(typeof result.current[1]).toBe('boolean');
1073+
expect(typeof result.current[2]).toBe('boolean');
1074+
});
1075+
1076+
it('Log error when track method is called and optimizely instance is not provided', () => {
1077+
const wrapper = ({ children }: { children: React.ReactNode }): React.ReactElement => {
1078+
//@ts-ignore
1079+
return <OptimizelyProvider>{children}</OptimizelyProvider>;
1080+
};
1081+
const { result } = renderHook(() => useTrackEvent(), { wrapper });
1082+
result.current[0]('eventKey');
1083+
expect(hooksLogger.error).toHaveBeenCalledTimes(1);
1084+
});
1085+
1086+
it('Log error when track method is called and client is not ready', () => {
1087+
optimizelyMock.isReady = jest.fn().mockReturnValue(false);
1088+
1089+
const wrapper = ({ children }: { children: React.ReactNode }): React.ReactElement => (
1090+
<OptimizelyProvider optimizely={optimizelyMock} isServerSide={false} timeout={10000}>
1091+
{children}
1092+
</OptimizelyProvider>
1093+
);
1094+
1095+
const { result } = renderHook(() => useTrackEvent(), { wrapper });
1096+
result.current[0]('eventKey');
1097+
expect(hooksLogger.error).toHaveBeenCalledTimes(1);
1098+
});
1099+
});
10511100
});

src/hooks.ts

+54-1
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ import { notifier } from './notifier';
2323
import { OptimizelyContext } from './Context';
2424
import { areAttributesEqual, OptimizelyDecision, createFailedDecision } from './utils';
2525

26-
const hooksLogger = getLogger('ReactSDK');
26+
export const hooksLogger = getLogger('ReactSDK');
2727

2828
enum HookType {
2929
EXPERIMENT = 'Experiment',
@@ -87,6 +87,10 @@ interface UseDecision {
8787
];
8888
}
8989

90+
interface UseTrackEvent {
91+
(): [(...args: Parameters<ReactSDKClient['track']>) => void, boolean, boolean];
92+
}
93+
9094
interface DecisionInputs {
9195
entityKey: string;
9296
overrideUserId?: string;
@@ -500,3 +504,52 @@ export const useDecision: UseDecision = (flagKey, options = {}, overrides = {})
500504

501505
return [state.decision, state.clientReady, state.didTimeout];
502506
};
507+
508+
export const useTrackEvent: UseTrackEvent = () => {
509+
const { optimizely, isServerSide, timeout } = useContext(OptimizelyContext);
510+
const isClientReady = !!(isServerSide || optimizely?.isReady());
511+
512+
const track = useCallback(
513+
(...rest: Parameters<ReactSDKClient['track']>): void => {
514+
if (!optimizely) {
515+
hooksLogger.error(`Unable to track events. optimizely prop must be supplied via a parent <OptimizelyProvider>`);
516+
return;
517+
}
518+
if (!isClientReady) {
519+
hooksLogger.error(`Unable to track events. Optimizely client is not ready yet.`);
520+
return;
521+
}
522+
optimizely.track(...rest);
523+
},
524+
[optimizely, isClientReady]
525+
);
526+
527+
if (!optimizely) {
528+
return [track, false, false];
529+
}
530+
531+
const [state, setState] = useState<{
532+
clientReady: boolean;
533+
didTimeout: DidTimeout;
534+
}>(() => {
535+
return {
536+
clientReady: isClientReady,
537+
didTimeout: false,
538+
};
539+
});
540+
541+
useEffect(() => {
542+
// Subscribe to initialization promise only
543+
// 1. When client is using Sdk Key, which means the initialization will be asynchronous
544+
// and we need to wait for the promise and update decision.
545+
// 2. When client is using datafile only but client is not ready yet which means user
546+
// was provided as a promise and we need to subscribe and wait for user to become available.
547+
if (optimizely.getIsUsingSdkKey() || !isClientReady) {
548+
subscribeToInitialization(optimizely, timeout, initState => {
549+
setState(initState);
550+
});
551+
}
552+
}, []);
553+
554+
return [track, state.clientReady, state.didTimeout];
555+
};

src/index.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/**
2-
* Copyright 2018-2019, 2023 Optimizely
2+
* Copyright 2018-2019, 2023, 2024 Optimizely
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -17,7 +17,7 @@
1717
export { OptimizelyContext, OptimizelyContextConsumer, OptimizelyContextProvider } from './Context';
1818
export { OptimizelyProvider } from './Provider';
1919
export { OptimizelyFeature } from './Feature';
20-
export { useFeature, useExperiment, useDecision } from './hooks';
20+
export { useFeature, useExperiment, useDecision, useTrackEvent } from './hooks';
2121
export { withOptimizely, WithOptimizelyProps, WithoutOptimizelyProps } from './withOptimizely';
2222
export { OptimizelyExperiment } from './Experiment';
2323
export { OptimizelyVariation } from './Variation';

0 commit comments

Comments
 (0)