From 69c869347218ac8e1598ea842e07061a82ebe05f Mon Sep 17 00:00:00 2001 From: Marcos Passos Date: Wed, 23 Feb 2022 22:29:04 -0300 Subject: [PATCH] Bump SDK to v0.8 (#115) --- README.md | 1 + docs/plug.md | 3 +- docs/testing.md | 109 +++++++++++++++++++++++++++++++++++++++ package-lock.json | 44 ++++++++-------- package.json | 2 +- src/plug.ts | 7 ++- src/sdk/tracking.ts | 7 ++- test/plug.test.ts | 121 ++++++++++++++++++++++++++++++++++++-------- 8 files changed, 249 insertions(+), 45 deletions(-) create mode 100644 docs/testing.md diff --git a/README.md b/README.md index 0b409ef0..926c141d 100644 --- a/README.md +++ b/README.md @@ -138,6 +138,7 @@ The following references provide guidance to help you get started, integrate, an - [Patch Reference](docs/patch.md) - [User Reference](docs/user.md) - [Session Reference](docs/session.md) +- [Testing](docs/testing.md) - [Troubleshooting](docs/troubleshooting.md) If you are new to the Croct platform, the [quick start guide](docs/quick-start.md) is a good starting point for diff --git a/docs/plug.md b/docs/plug.md index 8fe52622..7c8a1876 100644 --- a/docs/plug.md +++ b/docs/plug.md @@ -41,7 +41,8 @@ These are the currently supported options: | Option | Type | Required | Default Value | Description | |-------------------------|--------------|----------|---------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | `appId` | string | Depends | None | The ID of the application you set up on Croct. This option is required unless you have loaded the SDK using a HTML snippet that already specifies the application ID. | -| `debug` | boolean | No | `false` | If `true`, turns on debug mode, which logs helpful messages to the console. | +| `debug` | boolean | No | `false` | If `true`, turns on debug mode, which logs helpful messages to the console. See [Testing](testing.md#debug-mode) for more details. | +| `test` | boolean | No | `false` | If `true`, enables the test mode. See [Testing](testing.md#test-mode) for more details. | | `track` | boolean | No | `true` | If `true`, enables the automatic event tracking on initialization. | | `token` | string\|null | No | None | The JWT token issued by Croct. If `null`, clears any token specified on previous calls. | | `userId` | string | No | None | The ID of the user logged into the application. Internally, the SDK will issue a token using the specified ID as the subject claim of the token. The `token` and `userId` options are mutually exclusive. | diff --git a/docs/testing.md b/docs/testing.md new file mode 100644 index 00000000..45b86f70 --- /dev/null +++ b/docs/testing.md @@ -0,0 +1,109 @@ +# Testing + +For an enhanced developer experience, the SDK provides both debug and test modes to assist you with testing. + +## Debug mode + +The debug mode enables fine-grained logging to help developers detect and diagnose issues with the integration. +Each log receives a severity level, so you can filter the log output to only see the log messages you +care about. + +The severity levels and their respective meanings are: + +- 🧐 **Debug** + Fine-grained messages that provide context to understand the steps leading to errors and warnings. +- 🤓 **Info** + Informational messages that highlight the SDK's state and progress. +- 🤔 **Warning** + Warnings about potential issues that might be problems or might not. +- 😱 **Error** + Abnormal or unexpected behaviors that need attention. + +### Enabling debug mode + +To enable the debug mode, you need to set `debug` to true when initializing the SDK: + +```ts +croct.plug({debug: true}); +``` + +You can now check the console output at runtime for the debug logs. + +## Test mode + +The test mode enables the SDK to track events in test environments to ensure that the integration is working +as expected. It works by replacing the actual transport layer with a fake one to simulate successful calls. + +### Enabling test mode + +> ✨ If you use Jest or any other testing framework that sets the `NODE_ENV=test`, it should just work out of the box +> without any additional configuration. + +By default, the SDK automatically detects test environments based on the `NODE_ENV`. To explicitly enable or disable +the test mode, you can either: + +- Pass `test` as `true` when initializing the SDK +- Set the `CROCT_TEST_MODE` environment variable to `true` + +The order of precedence is as follows: + +1. If the `test` option is passed, that overrides any other environment settings +2. If the `CROCT_TEST_MODE` environment variable is set, that takes precedence over the automatic detection of +test environments +3. If neither `test` nor `CROCT_TEST_MODE` is set, the SDK detects the test environment automatically based on +the `NODE_ENV` + +### Testing events + +The SDK tracks an event for every operation executed on the server side. + +For example, executing the code below will trigger the `userProfileChanged` event with changes to the user profile: + +```ts +croct.user.edit() + .add('interest', 'tests') + .save() +``` + +This flexible design allows you to listen to events and test your integration easily. + +Let's take the previous code as an example. You can check if your integration is working as expected by listening to +the `userProfileChanged` event as follows: + +```ts +import {EventListener, EventInfo} from '@croct/plug/sdk/tracking'; + +test('should add an interest to the user profile', async () => { + await croct.plug({ + appId: '00000000-0000-0000-0000-000000000000', + }); + + const listener: EventListener = jest.fn(); + + croct.tracker.addListener(listener); + + await croct.user.edit() + .add('interest', 'tests') + .save(); + + expect(listener).toHaveBeenCalledWith( + expect.objectContaining>>({ + status: 'confirmed', + event: { + type: 'userProfileChanged', + patch: { + operations: [ + { + path: 'interest', + type: 'add', + value: 'tests', + }, + ], + }, + }, + }), + ); +}) +``` + +See the [Event reference](events.md) for the list of events triggered by the SDK. diff --git a/package-lock.json b/package-lock.json index 745a6325..e1360b97 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,7 +9,7 @@ "version": "0.0.0-dev", "license": "MIT", "dependencies": { - "@croct/sdk": "^0.7.0", + "@croct/sdk": "^0.8.0", "tslib": "^2.2.0" }, "devDependencies": { @@ -1470,11 +1470,11 @@ } }, "node_modules/@croct/sdk": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/@croct/sdk/-/sdk-0.7.0.tgz", - "integrity": "sha512-WC+ng2aEm8D4Yu/9CgslYE3FPD+lkW7EGILHhmyr7iYrNADKVShWpyDTPOCakBVuiPEkYIkND67G9BrDPG6f3Q==", + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/@croct/sdk/-/sdk-0.8.0.tgz", + "integrity": "sha512-ds/G1YA5GuSW1Jlj+HIez4qQWZwan35AFeUQBEJ6dOW/NRfe7ltfNa6jKCvNDHP/XRoeoUAKeuJV1t5OlVl0yg==", "dependencies": { - "tslib": "^2.2.0" + "tslib": "^2.3.1" }, "engines": { "node": ">=10" @@ -2970,9 +2970,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001228", - "resolved": "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001228.tgz", - "integrity": "sha512-QQmLOGJ3DEgokHbMSA8cj2a+geXqmnpyOFT0lhQV6P3/YOJvGDEwoedcwxEQ30gJIwIIunHIicunJ2rzK5gB2A==", + "version": "1.0.30001312", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001312.tgz", + "integrity": "sha512-Wiz1Psk2MEK0pX3rUzWaunLTZzqS2JYZFzNKqAiJGiuxIjRPLgV6+VDPOg6lQOUxmDwhTlh198JsTTi8Hzw6aQ==", "dev": true, "funding": { "type": "opencollective", @@ -8218,6 +8218,7 @@ "version": "0.5.3", "resolved": "https://registry.yarnpkg.com/source-map-resolve/-/source-map-resolve-0.5.3.tgz", "integrity": "sha512-Htz+RnsXWk5+P2slx5Jh3Q66vhQj1Cllm0zvnaY98+NFx+Dv2CF/f5O/t8x+KaNdrdIAsruNzoh/KpialbqAnw==", + "deprecated": "See https://github.com/lydell/source-map-resolve#deprecated", "dev": true, "dependencies": { "atob": "^2.1.2", @@ -8241,6 +8242,7 @@ "version": "0.4.1", "resolved": "https://registry.yarnpkg.com/source-map-url/-/source-map-url-0.4.1.tgz", "integrity": "sha512-cPiFOTLUKvJFIg4SKVScy4ilPPW6rFgMgfuZJPNoDuMs3nC1HbMUycBoJw77xFIp6z1UJQJOfx6C9GMH80DiTw==", + "deprecated": "See https://github.com/lydell/source-map-url#deprecated", "dev": true }, "node_modules/sourcemap-codec": { @@ -8849,9 +8851,9 @@ } }, "node_modules/tslib": { - "version": "2.2.0", - "resolved": "https://registry.yarnpkg.com/tslib/-/tslib-2.2.0.tgz", - "integrity": "sha512-gS9GVHRU+RGn5KQM2rllAlR3dU6m7AcpJKdtH8gFvQiC4Otgk98XnmMU+nZenHt/+VhnBPWwgrJsyrdcw6i23w==" + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.1.tgz", + "integrity": "sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw==" }, "node_modules/tsutils": { "version": "3.21.0", @@ -10656,11 +10658,11 @@ } }, "@croct/sdk": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/@croct/sdk/-/sdk-0.7.0.tgz", - "integrity": "sha512-WC+ng2aEm8D4Yu/9CgslYE3FPD+lkW7EGILHhmyr7iYrNADKVShWpyDTPOCakBVuiPEkYIkND67G9BrDPG6f3Q==", + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/@croct/sdk/-/sdk-0.8.0.tgz", + "integrity": "sha512-ds/G1YA5GuSW1Jlj+HIez4qQWZwan35AFeUQBEJ6dOW/NRfe7ltfNa6jKCvNDHP/XRoeoUAKeuJV1t5OlVl0yg==", "requires": { - "tslib": "^2.2.0" + "tslib": "^2.3.1" } }, "@eslint/eslintrc": { @@ -11813,9 +11815,9 @@ "dev": true }, "caniuse-lite": { - "version": "1.0.30001228", - "resolved": "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001228.tgz", - "integrity": "sha512-QQmLOGJ3DEgokHbMSA8cj2a+geXqmnpyOFT0lhQV6P3/YOJvGDEwoedcwxEQ30gJIwIIunHIicunJ2rzK5gB2A==", + "version": "1.0.30001312", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001312.tgz", + "integrity": "sha512-Wiz1Psk2MEK0pX3rUzWaunLTZzqS2JYZFzNKqAiJGiuxIjRPLgV6+VDPOg6lQOUxmDwhTlh198JsTTi8Hzw6aQ==", "dev": true }, "capture-exit": { @@ -16379,9 +16381,9 @@ } }, "tslib": { - "version": "2.2.0", - "resolved": "https://registry.yarnpkg.com/tslib/-/tslib-2.2.0.tgz", - "integrity": "sha512-gS9GVHRU+RGn5KQM2rllAlR3dU6m7AcpJKdtH8gFvQiC4Otgk98XnmMU+nZenHt/+VhnBPWwgrJsyrdcw6i23w==" + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.1.tgz", + "integrity": "sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw==" }, "tsutils": { "version": "3.21.0", diff --git a/package.json b/package.json index 6aaa77fb..17ca8351 100644 --- a/package.json +++ b/package.json @@ -33,7 +33,7 @@ "bundle": "rollup -c" }, "dependencies": { - "@croct/sdk": "^0.7.0", + "@croct/sdk": "^0.8.0", "tslib": "^2.2.0" }, "devDependencies": { diff --git a/src/plug.ts b/src/plug.ts index 0a1f5f81..151b5a97 100644 --- a/src/plug.ts +++ b/src/plug.ts @@ -124,11 +124,16 @@ export class GlobalPlug implements Plug { ); } - const {plugins, ...sdkConfiguration} = configuration; + const {plugins, test, ...sdkConfiguration} = configuration; const sdk = SdkFacade.init({ ...sdkConfiguration, appId: appId, + test: test ?? (typeof process === 'object' && ( + process.env?.CROCT_TEST_MODE !== undefined + ? process.env.CROCT_TEST_MODE === 'true' + : process.env?.NODE_ENV === 'test' + )), }); this.instance = sdk; diff --git a/src/sdk/tracking.ts b/src/sdk/tracking.ts index f6c1511b..26cb4879 100644 --- a/src/sdk/tracking.ts +++ b/src/sdk/tracking.ts @@ -1,5 +1,8 @@ +import {EventInfo as SdkEventInfo} from '@croct/sdk/tracker'; +import {TrackingEvent, TrackingEventType} from '@croct/sdk/trackingEvents'; + export {TrackerFacade} from '@croct/sdk/facade/trackerFacade'; -export {EventInfo, EventListener} from '@croct/sdk/tracker'; +export {EventListener} from '@croct/sdk/tracker'; export { TrackingEvent, TrackingEventType, @@ -7,3 +10,5 @@ export { ExternalTrackingEventPayload, ExternalTrackingEventType, } from '@croct/sdk/trackingEvents'; + +export type EventInfo = SdkEventInfo>; diff --git a/test/plug.test.ts b/test/plug.test.ts index ceb0ab41..6d3316ee 100644 --- a/test/plug.test.ts +++ b/test/plug.test.ts @@ -15,10 +15,15 @@ jest.mock('../src/constants', () => { describe('The Croct plug', () => { const APP_ID = '7e9d59a9-e4b3-45d4-b1c7-48287f1e5e8a'; const APP_CDN_URL = `${CDN_URL}?appId=${APP_ID}`; + const ENV_VARS = process.env; let croct: GlobalPlug; beforeEach(() => { + process.env = {...ENV_VARS}; + + delete process.env.NODE_ENV; + croct = new GlobalPlug(); delete window.croctEap; @@ -26,6 +31,7 @@ describe('The Croct plug', () => { afterEach(async () => { jest.restoreAllMocks(); + process.env = ENV_VARS; await croct.unplug(); }); @@ -54,7 +60,11 @@ describe('The Croct plug', () => { window.document.head.appendChild(script); - const config: SdkFacadeConfiguration = {appId: APP_ID}; + const config: SdkFacadeConfiguration = { + appId: APP_ID, + test: false, + }; + const sdkFacade = SdkFacade.init(config); const initialize = jest.spyOn(SdkFacade, 'init').mockReturnValue(sdkFacade); @@ -105,6 +115,7 @@ describe('The Croct plug', () => { appId: APP_ID, track: false, debug: false, + test: true, tokenScope: 'isolated', userId: 'c4r0l', eventMetadata: { @@ -120,6 +131,76 @@ describe('The Croct plug', () => { expect(initialize).toBeCalledWith(config); }); + test.each([ + 'test', + 'development', + 'production', + ])('should enable the test mode on test environments', environment => { + const config: SdkFacadeConfiguration = { + appId: APP_ID, + }; + + process.env.NODE_ENV = environment; + + const sdkFacade = SdkFacade.init(config); + const initialize = jest.spyOn(SdkFacade, 'init').mockReturnValue(sdkFacade); + + croct.plug(config); + + expect(initialize).toBeCalledWith(expect.objectContaining({test: environment === 'test'})); + }); + + test.each([ + true, + false, + ])('should enable the test mode based on CROCT_TEST_MODE environment variable', value => { + const config: SdkFacadeConfiguration = { + appId: APP_ID, + }; + + process.env.CROCT_TEST_MODE = `${value}`; + + const sdkFacade = SdkFacade.init(config); + const initialize = jest.spyOn(SdkFacade, 'init').mockReturnValue(sdkFacade); + + croct.plug(config); + + expect(initialize).toBeCalledWith(expect.objectContaining({test: value})); + }); + + test('should prioritize the specified test mode over environment variables', () => { + const config: SdkFacadeConfiguration = { + appId: APP_ID, + test: false, + }; + + process.env.NODE_ENV = 'test'; + process.env.CROCT_TEST_MODE = 'true'; + + const sdkFacade = SdkFacade.init(config); + const initialize = jest.spyOn(SdkFacade, 'init').mockReturnValue(sdkFacade); + + croct.plug(config); + + expect(initialize).toBeCalledWith(expect.objectContaining({test: false})); + }); + + test('should prioritize the test mode specified via CROCT_TEST_MODE over NODE_ENV', () => { + const config: SdkFacadeConfiguration = { + appId: APP_ID, + }; + + process.env.NODE_ENV = 'test'; + process.env.CROCT_TEST_MODE = 'false'; + + const sdkFacade = SdkFacade.init(config); + const initialize = jest.spyOn(SdkFacade, 'init').mockReturnValue(sdkFacade); + + croct.plug(config); + + expect(initialize).toBeCalledWith(expect.objectContaining({test: false})); + }); + test('should call the EAP initialization hook', () => { window.croctEap = { initialize: jest.fn().mockImplementation(function initialize(this: Plug) { @@ -253,7 +334,7 @@ describe('The Croct plug', () => { }, }); - expect(initialize).toBeCalledWith(config); + expect(initialize).toBeCalledWith(expect.objectContaining(config)); expect(fooFactory).toBeCalledWith(expect.objectContaining({options: {}})); expect(barFactory).toBeCalledWith(expect.objectContaining({options: {flag: true}})); @@ -331,7 +412,7 @@ describe('The Croct plug', () => { croct.plug(config); croct.plug(config); - expect(initialize).toBeCalledWith(config); + expect(initialize).toBeCalledWith(expect.objectContaining(config)); expect(initialize).toBeCalledTimes(1); }); @@ -413,7 +494,7 @@ describe('The Croct plug', () => { croct.plug(config); - expect(initialize).toBeCalledWith(config); + expect(initialize).toBeCalledWith(expect.objectContaining(config)); expect(croct.tracker).toBe(sdkFacade.tracker); }); @@ -430,7 +511,7 @@ describe('The Croct plug', () => { croct.plug(config); - expect(initialize).toBeCalledWith(config); + expect(initialize).toBeCalledWith(expect.objectContaining(config)); expect(croct.user).toBe(sdkFacade.user); }); @@ -447,7 +528,7 @@ describe('The Croct plug', () => { croct.plug(config); - expect(initialize).toBeCalledWith(config); + expect(initialize).toBeCalledWith(expect.objectContaining(config)); expect(croct.evaluator).toBe(sdkFacade.evaluator); }); @@ -464,7 +545,7 @@ describe('The Croct plug', () => { croct.plug(config); - expect(initialize).toBeCalledWith(config); + expect(initialize).toBeCalledWith(expect.objectContaining(config)); expect(croct.session).toBe(sdkFacade.session); }); @@ -481,7 +562,7 @@ describe('The Croct plug', () => { croct.plug(config); - expect(initialize).toBeCalledWith(config); + expect(initialize).toBeCalledWith(expect.objectContaining(config)); expect(croct.isAnonymous()).toBe(sdkFacade.user.isAnonymous()); }); @@ -502,7 +583,7 @@ describe('The Croct plug', () => { croct.plug(config); - expect(initialize).toBeCalledWith(config); + expect(initialize).toBeCalledWith(expect.objectContaining(config)); expect(croct.getUserId()).toBe(config.userId); }); @@ -520,7 +601,7 @@ describe('The Croct plug', () => { croct.plug(config); - expect(initialize).toBeCalledWith(config); + expect(initialize).toBeCalledWith(expect.objectContaining(config)); expect(() => croct.identify(1235 as unknown as string)).toThrow('The user ID must be a string.'); }); @@ -534,7 +615,7 @@ describe('The Croct plug', () => { croct.plug(config); - expect(initialize).toBeCalledWith(config); + expect(initialize).toBeCalledWith(expect.objectContaining(config)); croct.identify('3r1ck'); @@ -557,7 +638,7 @@ describe('The Croct plug', () => { croct.plug(config); - expect(initialize).toBeCalledWith(config); + expect(initialize).toBeCalledWith(expect.objectContaining(config)); expect(croct.isAnonymous()).toBeFalsy(); @@ -578,7 +659,7 @@ describe('The Croct plug', () => { croct.plug(config); - expect(initialize).toBeCalledWith(config); + expect(initialize).toBeCalledWith(expect.objectContaining(config)); const setToken = jest.spyOn(sdkFacade, 'setToken'); @@ -603,7 +684,7 @@ describe('The Croct plug', () => { croct.plug(config); - expect(initialize).toBeCalledWith(config); + expect(initialize).toBeCalledWith(expect.objectContaining(config)); const unsetToken = jest.spyOn(sdkFacade, 'unsetToken'); @@ -624,7 +705,7 @@ describe('The Croct plug', () => { croct.plug(config); - expect(initialize).toBeCalledWith(config); + expect(initialize).toBeCalledWith(expect.objectContaining(config)); const track = jest.spyOn(sdkFacade.tracker, 'track').mockResolvedValue({ type: 'userSignedUp', @@ -648,7 +729,7 @@ describe('The Croct plug', () => { croct.plug(config); - expect(initialize).toBeCalledWith(config); + expect(initialize).toBeCalledWith(expect.objectContaining(config)); const evaluate = jest.spyOn(sdkFacade.evaluator, 'evaluate').mockResolvedValue('carol'); @@ -671,7 +752,7 @@ describe('The Croct plug', () => { croct.plug(config); - expect(initialize).toBeCalledWith(config); + expect(initialize).toBeCalledWith(expect.objectContaining(config)); const evaluate = jest.spyOn(sdkFacade.evaluator, 'evaluate').mockResolvedValue(true); @@ -690,7 +771,7 @@ describe('The Croct plug', () => { croct.plug(config); - expect(initialize).toBeCalledWith(config); + expect(initialize).toBeCalledWith(expect.objectContaining(config)); const evaluate = jest.spyOn(sdkFacade.evaluator, 'evaluate').mockResolvedValue('foo'); @@ -709,7 +790,7 @@ describe('The Croct plug', () => { croct.plug(config); - expect(initialize).toBeCalledWith(config); + expect(initialize).toBeCalledWith(expect.objectContaining(config)); const evaluate = jest.spyOn(sdkFacade.evaluator, 'evaluate').mockRejectedValue(undefined); @@ -814,7 +895,7 @@ describe('The Croct plug', () => { croct.plug(config); - expect(initialize).toBeCalledWith(config); + expect(initialize).toBeCalledWith(expect.objectContaining(config)); const close = jest.spyOn(sdkFacade, 'close');