From 0449d48f008b40409c4c502bc77a02d506adfc4c Mon Sep 17 00:00:00 2001 From: Ryan Wang Date: Mon, 26 Jun 2023 18:45:05 -0400 Subject: [PATCH 1/8] add pages API to JS spec --- src/NativeFullStory.ts | 4 ++++ src/index.d.ts | 4 ++++ src/index.js | 14 +++++++++++++- 3 files changed, 21 insertions(+), 1 deletion(-) diff --git a/src/NativeFullStory.ts b/src/NativeFullStory.ts index 580c973..3fecf2a 100644 --- a/src/NativeFullStory.ts +++ b/src/NativeFullStory.ts @@ -15,6 +15,10 @@ export interface Spec extends TurboModule { restart(): void; log(logLevel: number, message: string): void; resetIdleTimer(): void; + createPage(pageName: string, pageProperties?: Object): void; + startPage(pageProperties?: Object): void; + endPage(): void; + updatePage(pageProperties: Object): void; } export default TurboModuleRegistry.get('FullStory'); diff --git a/src/index.d.ts b/src/index.d.ts index 0b3eca5..bc0641a 100644 --- a/src/index.d.ts +++ b/src/index.d.ts @@ -33,6 +33,10 @@ declare type FullStoryStatic = { restart(): void; log(logLevel: LogLevel, message: string): void; resetIdleTimer(): void; + createPage(pageName: string, pageProperties?: Object): void; + startPage(pageProperties?: Object): void; + endPage(): void; + updatePage(pageProperties: Object): void; }; declare global { diff --git a/src/index.js b/src/index.js index 8ca9715..eb15872 100644 --- a/src/index.js +++ b/src/index.js @@ -19,6 +19,10 @@ const { restart, log, resetIdleTimer, + createPage, + startPage, + endPage, + updatePage, } = FullStory; const LogLevel = { @@ -30,9 +34,13 @@ const LogLevel = { Assert: 5, // Clamps to Error on Android }; +const createPageWithProperties = (pageName, pageProperties = {}) => createPage(pageName, pageProperties); +const startPageWithProperties = (pageProperties = {}) => startPage(pageProperties); +const identifyWithProperties = (uid, userVars = {}) => identify(uid, userVars); + export default { anonymize, - identify, + identify: identifyWithProperties, setUserVars, onReady, getCurrentSession, @@ -44,4 +52,8 @@ export default { log, resetIdleTimer, LogLevel, + createPage: createPageWithProperties, + startPage: startPageWithProperties, + endPage, + updatePage, }; From ee3340fe8df2387d86191f11869c0c7896ae7c6b Mon Sep 17 00:00:00 2001 From: Ryan Wang Date: Mon, 26 Jun 2023 18:45:46 -0400 Subject: [PATCH 2/8] add pages API to iOS --- ios/FullStory.mm | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/ios/FullStory.mm b/ios/FullStory.mm index 1810c46..402394b 100644 --- a/ios/FullStory.mm +++ b/ios/FullStory.mm @@ -10,6 +10,7 @@ @implementation FullStory { RCTPromiseResolveBlock onReadyPromise; + id _page; } RCT_EXPORT_MODULE() @@ -128,6 +129,40 @@ - (void) getCurrentSessionURL:(RCTPromiseResolveBlock)resolve reject:(RCTPromise }); } +RCT_EXPORT_METHOD(createPage:(NSString *)pageName pageProperties:(NSDictionary *)pageProperties) +{ + dispatch_async(dispatch_get_main_queue(), ^{ + self->_page = [FS pageWithName:pageName properties:pageProperties]; + }); +} + +RCT_EXPORT_METHOD(startPage:(NSDictionary *)pageProperties) +{ + if ([pageProperties count] == 0) { + dispatch_async(dispatch_get_main_queue(), ^{ + [self->_page start]; + }); + } else { + dispatch_async(dispatch_get_main_queue(), ^{ + [self->_page startWithPropertyUpdates:pageProperties]; + }); + } +} + +RCT_EXPORT_METHOD(endPage) +{ + dispatch_async(dispatch_get_main_queue(), ^{ + [self->_page end]; + }); +} + +RCT_EXPORT_METHOD(updatePage:(NSDictionary *)pageProperties) +{ + dispatch_async(dispatch_get_main_queue(), ^{ + [self->_page updateProperties:pageProperties]; + }); +} + - (void) fullstoryDidStartSession:(NSString *)sessionUrl { if (!onReadyPromise) return; From 2959a07e5ae58e5e67c72338ee0e70aa4277dc87 Mon Sep 17 00:00:00 2001 From: Ryan Wang Date: Mon, 26 Jun 2023 18:46:20 -0400 Subject: [PATCH 3/8] add Android pages API --- .../src/legacy/java/com/FullStoryModule.java | 20 ++++++++++++++++ .../reactnative/FullStoryModuleImpl.java | 24 +++++++++++++++++++ .../src/turbo/java/com/FullStoryModule.java | 20 ++++++++++++++++ 3 files changed, 64 insertions(+) diff --git a/android/src/legacy/java/com/FullStoryModule.java b/android/src/legacy/java/com/FullStoryModule.java index 617a5bd..5ac6dea 100644 --- a/android/src/legacy/java/com/FullStoryModule.java +++ b/android/src/legacy/java/com/FullStoryModule.java @@ -86,4 +86,24 @@ public static void log(double level, String message) { public static void resetIdleTimer() { FullStoryModuleImpl.resetIdleTimer(); } + + @ReactMethod + public static void createPage(String pageName, ReadableMap pageProperties) { + FullStoryModuleImpl.createPage(pageName, pageProperties); + } + + @ReactMethod + public static void startPage(ReadableMap pageProperties) { + FullStoryModuleImpl.startPage(pageProperties); + } + + @ReactMethod + public static void updatePage(ReadableMap pageProperties) { + FullStoryModuleImpl.updatePage(pageProperties); + } + + @ReactMethod + public static void endPage() { + FullStoryModuleImpl.endPage(); + } } diff --git a/android/src/main/java/com/fullstory/reactnative/FullStoryModuleImpl.java b/android/src/main/java/com/fullstory/reactnative/FullStoryModuleImpl.java index 118a6f7..e4f37f6 100644 --- a/android/src/main/java/com/fullstory/reactnative/FullStoryModuleImpl.java +++ b/android/src/main/java/com/fullstory/reactnative/FullStoryModuleImpl.java @@ -5,6 +5,7 @@ import com.facebook.react.bridge.ReadableMap; import com.facebook.react.bridge.WritableMap; import com.fullstory.FS; +import com.fullstory.FSPage; import com.fullstory.FSOnReadyListener; import com.fullstory.FSSessionData; @@ -14,6 +15,7 @@ public class FullStoryModuleImpl { public static final String NAME = "FullStory"; + private static FSPage page; public static void anonymize() { FS.anonymize(); @@ -137,4 +139,26 @@ private static Map toMap(ReadableMap map) { return map.toHashMap(); } + + public static void createPage(String pageName, ReadableMap pageProperties) { + page = FS.page(pageName, toMap(pageProperties)); + } + + public static void startPage(ReadableMap pageProperties) { + if (page != null) { + page.start(toMap(pageProperties)); + } + } + + public static void updatePage(ReadableMap pageProperties) { + if (page != null) { + page.updateProperties(toMap(pageProperties)); + } + } + + public static void endPage() { + if (page != null) { + page.end(); + } + } } diff --git a/android/src/turbo/java/com/FullStoryModule.java b/android/src/turbo/java/com/FullStoryModule.java index 44a0254..941431c 100644 --- a/android/src/turbo/java/com/FullStoryModule.java +++ b/android/src/turbo/java/com/FullStoryModule.java @@ -87,4 +87,24 @@ public void log(double level, String message) { public void resetIdleTimer() { FullStoryModuleImpl.resetIdleTimer(); } + + @Override + public void createPage(String pageName, ReadableMap pageProperties) { + FullStoryModuleImpl.createPage(pageName, pageProperties); + } + + @Override + public void startPage(ReadableMap pageProperties) { + FullStoryModuleImpl.startPage(pageProperties); + } + + @Override + public void updatePage(ReadableMap pageProperties) { + FullStoryModuleImpl.updatePage(pageProperties); + } + + @Override + public void endPage() { + FullStoryModuleImpl.endPage(); + } } From 396e9a82c9ca613a37179a3d5500d58497fb5cdd Mon Sep 17 00:00:00 2001 From: Ryan Wang Date: Thu, 31 Aug 2023 14:13:34 -0400 Subject: [PATCH 4/8] Add Android pages implementation (#68) * Add Android pages implementation * updates on feedback - update legacy RN native module method declarations - replace System.out with android.util.Log - initialize reflected methods once - getUUID is async - fix page properties merge - add TS FSPage types * cleanup and fix turbomodule * remove private functions from export * remove page methods from types * add error message * generate UUID with Math.random() * remove async method * update method type to synchronous * update android log --------- Co-authored-by: Ryan Wang --- .../src/legacy/java/com/FullStoryModule.java | 27 ++--- .../reactnative/FullStoryModuleImpl.java | 79 +++++++++++--- .../src/turbo/java/com/FullStoryModule.java | 28 ++--- src/FSPage.ts | 102 ++++++++++++++++++ src/NativeFullStory.ts | 7 +- src/index.d.ts | 19 +++- src/index.js | 12 +-- 7 files changed, 198 insertions(+), 76 deletions(-) create mode 100644 src/FSPage.ts diff --git a/android/src/legacy/java/com/FullStoryModule.java b/android/src/legacy/java/com/FullStoryModule.java index 5ac6dea..13a61c7 100644 --- a/android/src/legacy/java/com/FullStoryModule.java +++ b/android/src/legacy/java/com/FullStoryModule.java @@ -1,20 +1,10 @@ package com.fullstory.reactnative; -import androidx.annotation.NonNull; -import com.facebook.react.bridge.Arguments; import com.facebook.react.bridge.Promise; import com.facebook.react.bridge.ReactApplicationContext; import com.facebook.react.bridge.ReactContextBaseJavaModule; import com.facebook.react.bridge.ReactMethod; import com.facebook.react.bridge.ReadableMap; -import com.facebook.react.bridge.NativeModule; -import com.facebook.react.bridge.ReactContext; - -import java.util.Map; -import java.util.HashMap; - -import java.util.Map; -import java.util.concurrent.atomic.AtomicReference; public class FullStoryModule extends ReactContextBaseJavaModule { @@ -88,22 +78,17 @@ public static void resetIdleTimer() { } @ReactMethod - public static void createPage(String pageName, ReadableMap pageProperties) { - FullStoryModuleImpl.createPage(pageName, pageProperties); - } - - @ReactMethod - public static void startPage(ReadableMap pageProperties) { - FullStoryModuleImpl.startPage(pageProperties); + public static void startPage(String nonce, String pageName, ReadableMap pageProperties) { + FullStoryModuleImpl.startPage(nonce, pageName, pageProperties); } @ReactMethod - public static void updatePage(ReadableMap pageProperties) { - FullStoryModuleImpl.updatePage(pageProperties); + public static void updatePage(String uuid, ReadableMap pageProperties) { + FullStoryModuleImpl.updatePage(uuid, pageProperties); } @ReactMethod - public static void endPage() { - FullStoryModuleImpl.endPage(); + public static void endPage(String uuid) { + FullStoryModuleImpl.endPage(uuid); } } diff --git a/android/src/main/java/com/fullstory/reactnative/FullStoryModuleImpl.java b/android/src/main/java/com/fullstory/reactnative/FullStoryModuleImpl.java index e4f37f6..b893fae 100644 --- a/android/src/main/java/com/fullstory/reactnative/FullStoryModuleImpl.java +++ b/android/src/main/java/com/fullstory/reactnative/FullStoryModuleImpl.java @@ -1,21 +1,51 @@ package com.fullstory.reactnative; +import android.util.Log; + import com.facebook.react.bridge.Arguments; import com.facebook.react.bridge.Promise; import com.facebook.react.bridge.ReadableMap; import com.facebook.react.bridge.WritableMap; import com.fullstory.FS; -import com.fullstory.FSPage; import com.fullstory.FSOnReadyListener; import com.fullstory.FSSessionData; +import java.util.UUID; import java.util.Map; import java.util.concurrent.atomic.AtomicReference; - +import java.lang.reflect.Method; public class FullStoryModuleImpl { public static final String NAME = "FullStory"; - private static FSPage page; + private static final String TAG = "FullStoryModuleImpl"; + public static final boolean reflectionSuccess; + private static final Method PAGE_VIEW; + private static final Method UPDATE_PAGE_PROPERTIES; + private static final Method END_PAGE; + + static { + Method pageView; + Method updatePageProperties; + Method endPage; + try { + pageView = FS.class.getMethod("__pageView", UUID.class, String.class, Map.class); + updatePageProperties = FS.class.getMethod("__updatePageProperties", UUID.class, Map.class); + endPage = FS.class.getMethod("__endPage", UUID.class); + } catch (Throwable t) { + pageView = null; + updatePageProperties = null; + endPage = null; + Log.e(TAG, "Unable to access native FullStory pages API. Pages API will not function correctly. " + + "Make sure that your plugin is at least version 1.38; if the issue persists, please contact FullStory Support."); + } + + PAGE_VIEW = pageView; + UPDATE_PAGE_PROPERTIES = updatePageProperties; + END_PAGE = endPage; + + reflectionSuccess = PAGE_VIEW != null && UPDATE_PAGE_PROPERTIES != null && END_PAGE != null; + } + public static void anonymize() { FS.anonymize(); @@ -140,25 +170,44 @@ private static Map toMap(ReadableMap map) { return map.toHashMap(); } - public static void createPage(String pageName, ReadableMap pageProperties) { - page = FS.page(pageName, toMap(pageProperties)); - } + public static void startPage(String uuid, String pageName, ReadableMap pageProperties) { + if (!reflectionSuccess) { + return; + } - public static void startPage(ReadableMap pageProperties) { - if (page != null) { - page.start(toMap(pageProperties)); + UUID nonce = UUID.fromString(uuid); + try { + PAGE_VIEW.invoke(null, nonce, pageName, toMap(pageProperties)); + } catch (Throwable t) { + // this should never happen + Log.e(TAG, "Unexpected error while calling startPage. Please contact FullStory Support."); } } - public static void updatePage(ReadableMap pageProperties) { - if (page != null) { - page.updateProperties(toMap(pageProperties)); + public static void updatePage(String uuid, ReadableMap pageProperties) { + if (!reflectionSuccess) { + return; + } + UUID nonce = UUID.fromString(uuid); + + try { + UPDATE_PAGE_PROPERTIES.invoke(null, nonce, toMap(pageProperties)); + } catch (Throwable t) { + // this should never happen + Log.e(TAG, "Unexpected error while calling updatePage. Please contact FullStory Support."); } } - public static void endPage() { - if (page != null) { - page.end(); + public static void endPage(String uuid) { + if (!reflectionSuccess) { + return; + } + UUID nonce = UUID.fromString(uuid); + try { + END_PAGE.invoke(null, nonce); + } catch (Throwable t) { + // this should never happen + Log.e(TAG, "Unexpected error while calling endPage. Please contact FullStory Support."); } } } diff --git a/android/src/turbo/java/com/FullStoryModule.java b/android/src/turbo/java/com/FullStoryModule.java index 941431c..25c9f3e 100644 --- a/android/src/turbo/java/com/FullStoryModule.java +++ b/android/src/turbo/java/com/FullStoryModule.java @@ -1,20 +1,9 @@ package com.fullstory.reactnative; import androidx.annotation.NonNull; -import com.facebook.react.bridge.Arguments; import com.facebook.react.bridge.Promise; import com.facebook.react.bridge.ReactApplicationContext; -import com.facebook.react.bridge.ReactContextBaseJavaModule; -import com.facebook.react.bridge.ReactMethod; import com.facebook.react.bridge.ReadableMap; -import com.facebook.react.bridge.NativeModule; -import com.facebook.react.bridge.ReactContext; - -import java.util.Map; -import java.util.HashMap; - -import java.util.Map; -import java.util.concurrent.atomic.AtomicReference; public class FullStoryModule extends NativeFullStorySpec { @@ -89,22 +78,17 @@ public void resetIdleTimer() { } @Override - public void createPage(String pageName, ReadableMap pageProperties) { - FullStoryModuleImpl.createPage(pageName, pageProperties); - } - - @Override - public void startPage(ReadableMap pageProperties) { - FullStoryModuleImpl.startPage(pageProperties); + public void startPage(String nonce, String pageName, ReadableMap pageProperties) { + FullStoryModuleImpl.startPage(nonce, pageName, pageProperties); } @Override - public void updatePage(ReadableMap pageProperties) { - FullStoryModuleImpl.updatePage(pageProperties); + public void updatePage(String uuid, ReadableMap pageProperties) { + FullStoryModuleImpl.updatePage(uuid, pageProperties); } @Override - public void endPage() { - FullStoryModuleImpl.endPage(); + public void endPage(String uuid) { + FullStoryModuleImpl.endPage(uuid); } } diff --git a/src/FSPage.ts b/src/FSPage.ts new file mode 100644 index 0000000..0b57320 --- /dev/null +++ b/src/FSPage.ts @@ -0,0 +1,102 @@ +import { NativeModules } from 'react-native'; + +const isTurboModuleEnabled = global.__turboModuleProxy != null; + +const FullStory = isTurboModuleEnabled + ? require('./NativeFullStory').default + : NativeModules.FullStory; + +const { startPage, endPage, updatePage } = FullStory; + +export class FSPage { + private pageName: string; + private nonce: string; + private properties: Object; + + constructor(pageName: string, properties: Object = {}) { + this.pageName = pageName; + this.properties = properties; + this.cleanProperties(); + } + + private static FS_PAGE_NAME_KEY = 'pageName'; + + private static generateUUID() { + return 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx'.replace(/[x]/g, () => { + const char = Math.floor(Math.random() * 16); + return char.toString(16); + }); + } + + private static isObject(value) { + return value && typeof value === 'object' && !Array.isArray(value); + } + + private static merge(oldValue, newValue) { + // We can only merge dictionaries and do not perform recursion whenever we + // encounter a non-dictionary value + // (see com.fullstory.instrumentation FSPageImpl.java) + if (!FSPage.isObject(oldValue) || !FSPage.isObject(newValue)) { + return newValue; + } + return FSPage.mergeObjects(oldValue, newValue); + } + + private static mergeObjects(oldObj, newObj) { + // return new object instance on immutable "old" object frozen by RN + const mergedObj = { ...oldObj }; + for (const key in newObj) { + const oldInnerValue = oldObj[key]; + if (oldObj[key]) { + mergedObj[key] = FSPage.merge(oldInnerValue, newObj[key]); + } else { + mergedObj[key] = newObj[key]; + } + } + return mergedObj; + } + + private cleanProperties() { + if (this.properties && this.properties[FSPage.FS_PAGE_NAME_KEY]) { + delete this.properties[FSPage.FS_PAGE_NAME_KEY]; + console.warn(`${FSPage.FS_PAGE_NAME_KEY} is a reserved property and has been removed.`); + } + } + + update(properties: Object) { + if (!this.nonce) { + console.error( + 'Called `updateProperties` on FSPage that has not been `start`-ed. This may ' + + 'be a mistake. `updateProperties` should be called on the same FSPage ' + + 'instance that the corresponding `start` is called on.', + ); + return; + } + + this.properties = FSPage.merge(this.properties, properties); + this.cleanProperties(); + updatePage(this.nonce, this.properties); + } + + start(properties?: Object) { + if (properties) { + this.properties = FSPage.merge(this.properties, properties); + this.cleanProperties(); + } + this.nonce = FSPage.generateUUID(); + startPage(this.nonce, this.pageName, this.properties); + } + + end() { + if (!this.nonce) { + console.error( + 'Called `end` on FSPage that has not been `start`-ed. `end` should be ' + + 'called on the same FSPage instance that the corresponding `start` is ' + + 'called on.', + ); + return; + } + endPage(this.nonce); + this.nonce = ''; + } +} diff --git a/src/NativeFullStory.ts b/src/NativeFullStory.ts index 3fecf2a..bb380a4 100644 --- a/src/NativeFullStory.ts +++ b/src/NativeFullStory.ts @@ -15,10 +15,9 @@ export interface Spec extends TurboModule { restart(): void; log(logLevel: number, message: string): void; resetIdleTimer(): void; - createPage(pageName: string, pageProperties?: Object): void; - startPage(pageProperties?: Object): void; - endPage(): void; - updatePage(pageProperties: Object): void; + startPage(nonce: string, pageName: string, pageProperties?: Object): void; + endPage(uuid: string): void; + updatePage(uuid: string, pageProperties: Object): void; } export default TurboModuleRegistry.get('FullStory'); diff --git a/src/index.d.ts b/src/index.d.ts index bc0641a..dda3195 100644 --- a/src/index.d.ts +++ b/src/index.d.ts @@ -33,10 +33,6 @@ declare type FullStoryStatic = { restart(): void; log(logLevel: LogLevel, message: string): void; resetIdleTimer(): void; - createPage(pageName: string, pageProperties?: Object): void; - startPage(pageProperties?: Object): void; - endPage(): void; - updatePage(pageProperties: Object): void; }; declare global { @@ -49,5 +45,20 @@ declare global { } } +export declare class FSPage { + private pageName; + private nonce; + private properties; + constructor(pageName: string, properties?: Object); + private static FS_PAGE_NAME_KEY; + private static isObject; + private static merge; + private static mergeObjects; + private cleanProperties; + update(properties: Object): void; + start(properties?: Object): void; + end(): void; +} + declare const FullStory: FullStoryStatic; export default FullStory; diff --git a/src/index.js b/src/index.js index eb15872..0d3b7c4 100644 --- a/src/index.js +++ b/src/index.js @@ -19,10 +19,6 @@ const { restart, log, resetIdleTimer, - createPage, - startPage, - endPage, - updatePage, } = FullStory; const LogLevel = { @@ -34,10 +30,10 @@ const LogLevel = { Assert: 5, // Clamps to Error on Android }; -const createPageWithProperties = (pageName, pageProperties = {}) => createPage(pageName, pageProperties); -const startPageWithProperties = (pageProperties = {}) => startPage(pageProperties); const identifyWithProperties = (uid, userVars = {}) => identify(uid, userVars); +export { FSPage } from './FSPage'; + export default { anonymize, identify: identifyWithProperties, @@ -52,8 +48,4 @@ export default { log, resetIdleTimer, LogLevel, - createPage: createPageWithProperties, - startPage: startPageWithProperties, - endPage, - updatePage, }; From c6ff20edf157d1c22d697ba66ddd1c21f1a159c1 Mon Sep 17 00:00:00 2001 From: Ryan Wang Date: Wed, 6 Sep 2023 14:26:46 -0400 Subject: [PATCH 5/8] call private ios pages API (#69) * Add Android pages implementation * updates on feedback - update legacy RN native module method declarations - replace System.out with android.util.Log - initialize reflected methods once - getUUID is async - fix page properties merge - add TS FSPage types * cleanup and fix turbomodule * remove private functions from export * remove page methods from types * add error message * generate UUID with Math.random() * remove async method * update method type to synchronous * implement ios pages API * formatting * formatting * Fix rebase regression * Edit error message --------- Co-authored-by: Ryan Wang --- .../reactnative/FullStoryModuleImpl.java | 2 +- ios/FullStory.h | 6 +++ ios/FullStory.mm | 49 +++++++++++-------- 3 files changed, 35 insertions(+), 22 deletions(-) diff --git a/android/src/main/java/com/fullstory/reactnative/FullStoryModuleImpl.java b/android/src/main/java/com/fullstory/reactnative/FullStoryModuleImpl.java index b893fae..8f577b2 100644 --- a/android/src/main/java/com/fullstory/reactnative/FullStoryModuleImpl.java +++ b/android/src/main/java/com/fullstory/reactnative/FullStoryModuleImpl.java @@ -36,7 +36,7 @@ public class FullStoryModuleImpl { updatePageProperties = null; endPage = null; Log.e(TAG, "Unable to access native FullStory pages API. Pages API will not function correctly. " + - "Make sure that your plugin is at least version 1.38; if the issue persists, please contact FullStory Support."); + "Make sure that your plugin is at least version 1.41; if the issue persists, please contact FullStory Support."); } PAGE_VIEW = pageView; diff --git a/ios/FullStory.h b/ios/FullStory.h index 9deee71..7ac8a67 100644 --- a/ios/FullStory.h +++ b/ios/FullStory.h @@ -9,6 +9,12 @@ @interface FullStory : NSObject @end +@interface FS(FSPrivate) ++ (void) _pageViewWithNonce:(NSUUID *)nonce name:(NSString *)pageName properties:(NSDictionary *)properties; ++ (void) _updatePageWithNonce:(NSUUID *)nonce properties:(NSDictionary *)properties; ++ (void) _endPageWithNonce:(NSUUID *)nonce; +@end + #ifdef RCT_NEW_ARCH_ENABLED @interface FullStory () @end diff --git a/ios/FullStory.mm b/ios/FullStory.mm index 402394b..bd69b92 100644 --- a/ios/FullStory.mm +++ b/ios/FullStory.mm @@ -5,14 +5,16 @@ #import #import #import +#import #import "FSReactSwizzle.h" @implementation FullStory { RCTPromiseResolveBlock onReadyPromise; - id _page; } +NSString *const PagesAPIError = @"Unable to access native FullStory pages API and call %@. Pages API will not function correctly. Make sure that your plugin is at least version 1.41; if the issue persists, please contact FullStory Support."; + RCT_EXPORT_MODULE() RCT_EXPORT_METHOD(anonymize) @@ -129,38 +131,43 @@ - (void) getCurrentSessionURL:(RCTPromiseResolveBlock)resolve reject:(RCTPromise }); } -RCT_EXPORT_METHOD(createPage:(NSString *)pageName pageProperties:(NSDictionary *)pageProperties) +RCT_EXPORT_METHOD(startPage:(NSString *)nonce pageName:(NSString *)pageName pageProperties:(NSDictionary *)pageProperties) { - dispatch_async(dispatch_get_main_queue(), ^{ - self->_page = [FS pageWithName:pageName properties:pageProperties]; - }); -} + if (![FS respondsToSelector:@selector(_pageViewWithNonce:name:properties:)]) { + RCTLogError(PagesAPIError, @"startPage"); + } else { + NSUUID *uuid = [[NSUUID alloc] initWithUUIDString:nonce]; -RCT_EXPORT_METHOD(startPage:(NSDictionary *)pageProperties) -{ - if ([pageProperties count] == 0) { dispatch_async(dispatch_get_main_queue(), ^{ - [self->_page start]; + [FS _pageViewWithNonce:uuid name:pageName properties:pageProperties]; }); + } +} + +RCT_EXPORT_METHOD(endPage:(NSString *)nonce) +{ + if (![FS respondsToSelector:@selector(_endPageWithNonce:)]) { + RCTLogError(PagesAPIError, @"endPage"); } else { + NSUUID *uuid = [[NSUUID alloc] initWithUUIDString:nonce]; + dispatch_async(dispatch_get_main_queue(), ^{ - [self->_page startWithPropertyUpdates:pageProperties]; + [FS _endPageWithNonce:uuid]; }); } } -RCT_EXPORT_METHOD(endPage) +RCT_EXPORT_METHOD(updatePage:(NSString *)nonce pageProperties:(NSDictionary *)pageProperties) { - dispatch_async(dispatch_get_main_queue(), ^{ - [self->_page end]; - }); -} + if (![FS respondsToSelector:@selector(_updatePageWithNonce:properties:)]) { + RCTLogError(PagesAPIError, @"updatePage"); + } else { + NSUUID *uuid = [[NSUUID alloc] initWithUUIDString:nonce]; -RCT_EXPORT_METHOD(updatePage:(NSDictionary *)pageProperties) -{ - dispatch_async(dispatch_get_main_queue(), ^{ - [self->_page updateProperties:pageProperties]; - }); + dispatch_async(dispatch_get_main_queue(), ^{ + [FS _updatePageWithNonce:uuid properties:pageProperties]; + }); + } } - (void) fullstoryDidStartSession:(NSString *)sessionUrl { From 0b76aafbb97ad80244d76b3022fe0e61ccd8c26c Mon Sep 17 00:00:00 2001 From: Ryan Wang Date: Fri, 8 Sep 2023 13:26:26 -0400 Subject: [PATCH 6/8] Improve Pages types and add testing (#76) Co-authored-by: Ryan Wang --- babel.config.js | 3 + jest.config.js | 12 + package-lock.json | 623 +++++++++++++++++++++++++++++------ package.json | 6 +- setupTests.js | 4 + src/FSPage.ts | 30 +- src/__tests__/FSPage.test.ts | 104 ++++++ 7 files changed, 663 insertions(+), 119 deletions(-) create mode 100644 babel.config.js create mode 100644 jest.config.js create mode 100644 setupTests.js create mode 100644 src/__tests__/FSPage.test.ts diff --git a/babel.config.js b/babel.config.js new file mode 100644 index 0000000..f842b77 --- /dev/null +++ b/babel.config.js @@ -0,0 +1,3 @@ +module.exports = { + presets: ['module:metro-react-native-babel-preset'], +}; diff --git a/jest.config.js b/jest.config.js new file mode 100644 index 0000000..0cc6cb8 --- /dev/null +++ b/jest.config.js @@ -0,0 +1,12 @@ +module.exports = { + preset: 'react-native', + moduleFileExtensions: ['ts', 'tsx', 'js'], + setupFiles: ['/setupTests.js'], + testRegex: '(/__tests__/.*|\\.(test|spec))\\.(ts|tsx|js)$', + transform: { + '^.+\\.(js)$': 'babel-jest', + '\\.(ts|tsx)$': 'ts-jest', + }, + transformIgnorePatterns: ['/node_modules/(?!(@react-native|react-native)/).*/'], + testPathIgnorePatterns: ['/node_modules/', '/plugin'], +}; diff --git a/package-lock.json b/package-lock.json index f0138c2..255a3c8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,12 +13,14 @@ "@fullstory/babel-plugin-react-native": "^1.0.3" }, "devDependencies": { + "@babel/core": "^7.22.15", "@react-native-community/eslint-config": "^3.2.0", "@react-native/eslint-plugin-specs": "^0.72.4", "@tsconfig/node10": "^1.0.9", "@types/jest": "^28.1.4", "@typescript-eslint/eslint-plugin": "^6.4.0", "@typescript-eslint/parser": "^6.4.0", + "babel-jest": "^29.6.4", "eslint": "^8.47.0", "eslint-config-prettier": "^9.0.0", "eslint-config-universe": "^12.0.0", @@ -71,33 +73,33 @@ } }, "node_modules/@babel/compat-data": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.22.5.tgz", - "integrity": "sha512-4Jc/YuIaYqKnDDz892kPIledykKg12Aw1PYX5i/TY28anJtacvM1Rrr8wbieB9GfEJwlzqT0hUEao0CxEebiDA==", + "version": "7.22.9", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.22.9.tgz", + "integrity": "sha512-5UamI7xkUcJ3i9qVDS+KFDEK8/7oJ55/sJMB1Ge7IEapr7KfdfV/HErR+koZwOfd+SgtFKOKRhRakdg++DcJpQ==", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/core": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.22.5.tgz", - "integrity": "sha512-SBuTAjg91A3eKOvD+bPEz3LlhHZRNu1nFOVts9lzDJTXshHTjII0BAtDS3Y2DAkdZdDKWVZGVwkDfc4Clxn1dg==", + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.22.15.tgz", + "integrity": "sha512-PtZqMmgRrvj8ruoEOIwVA3yoF91O+Hgw9o7DAUTNBA6Mo2jpu31clx9a7Nz/9JznqetTR6zwfC4L3LAjKQXUwA==", "dependencies": { "@ampproject/remapping": "^2.2.0", - "@babel/code-frame": "^7.22.5", - "@babel/generator": "^7.22.5", - "@babel/helper-compilation-targets": "^7.22.5", - "@babel/helper-module-transforms": "^7.22.5", - "@babel/helpers": "^7.22.5", - "@babel/parser": "^7.22.5", - "@babel/template": "^7.22.5", - "@babel/traverse": "^7.22.5", - "@babel/types": "^7.22.5", + "@babel/code-frame": "^7.22.13", + "@babel/generator": "^7.22.15", + "@babel/helper-compilation-targets": "^7.22.15", + "@babel/helper-module-transforms": "^7.22.15", + "@babel/helpers": "^7.22.15", + "@babel/parser": "^7.22.15", + "@babel/template": "^7.22.15", + "@babel/traverse": "^7.22.15", + "@babel/types": "^7.22.15", "convert-source-map": "^1.7.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", - "json5": "^2.2.2", - "semver": "^6.3.0" + "json5": "^2.2.3", + "semver": "^6.3.1" }, "engines": { "node": ">=6.9.0" @@ -108,16 +110,81 @@ } }, "node_modules/@babel/core/node_modules/@babel/code-frame": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.22.5.tgz", - "integrity": "sha512-Xmwn266vad+6DAqEB2A6V/CcZVp62BbwVmcOJc2RPuwih1kw02TjQvWVWlcKGbBPd+8/0V5DEkOcizRGYsspYQ==", + "version": "7.22.13", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.22.13.tgz", + "integrity": "sha512-XktuhWlJ5g+3TJXc5upd9Ks1HutSArik6jf2eAjYFyIOf4ej3RN+184cZbzDvbPnuTJIUhPKKJE3cIsYTiAT3w==", "dependencies": { - "@babel/highlight": "^7.22.5" + "@babel/highlight": "^7.22.13", + "chalk": "^2.4.2" }, "engines": { "node": ">=6.9.0" } }, + "node_modules/@babel/core/node_modules/ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dependencies": { + "color-convert": "^1.9.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/core/node_modules/chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dependencies": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/core/node_modules/color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/@babel/core/node_modules/color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==" + }, + "node_modules/@babel/core/node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/@babel/core/node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/core/node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/@babel/eslint-parser": { "version": "7.22.10", "resolved": "https://registry.npmjs.org/@babel/eslint-parser/-/eslint-parser-7.22.10.tgz", @@ -146,11 +213,11 @@ } }, "node_modules/@babel/generator": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.22.5.tgz", - "integrity": "sha512-+lcUbnTRhd0jOewtFSedLyiPsD5tswKkbgcezOqqWFUVNEwoUTlpPOBmvhG7OXWLR4jMdv0czPGH5XbflnD1EA==", + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.22.15.tgz", + "integrity": "sha512-Zu9oWARBqeVOW0dZOjXc3JObrzuqothQ3y/n1kUtrjCoCPLkXUwMvOo/F/TCfoHMbWIFlWwpZtkZVb9ga4U2pA==", "dependencies": { - "@babel/types": "^7.22.5", + "@babel/types": "^7.22.15", "@jridgewell/gen-mapping": "^0.3.2", "@jridgewell/trace-mapping": "^0.3.17", "jsesc": "^2.5.1" @@ -182,21 +249,18 @@ } }, "node_modules/@babel/helper-compilation-targets": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.22.5.tgz", - "integrity": "sha512-Ji+ywpHeuqxB8WDxraCiqR0xfhYjiDE/e6k7FuIaANnoOFxAHskHChz4vA1mJC9Lbm01s1PVAGhQY4FUKSkGZw==", + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.22.15.tgz", + "integrity": "sha512-y6EEzULok0Qvz8yyLkCvVX+02ic+By2UdOhylwUOvOn9dvYc9mKICJuuU1n1XBI02YWsNsnrY1kc6DVbjcXbtw==", "dependencies": { - "@babel/compat-data": "^7.22.5", - "@babel/helper-validator-option": "^7.22.5", - "browserslist": "^4.21.3", + "@babel/compat-data": "^7.22.9", + "@babel/helper-validator-option": "^7.22.15", + "browserslist": "^4.21.9", "lru-cache": "^5.1.1", - "semver": "^6.3.0" + "semver": "^6.3.1" }, "engines": { "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" } }, "node_modules/@babel/helper-create-class-features-plugin": { @@ -296,32 +360,32 @@ } }, "node_modules/@babel/helper-module-imports": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.22.5.tgz", - "integrity": "sha512-8Dl6+HD/cKifutF5qGd/8ZJi84QeAKh+CEe1sBzz8UayBBGg1dAIJrdHOcOM5b2MpzWL2yuotJTtGjETq0qjXg==", + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.22.15.tgz", + "integrity": "sha512-0pYVBnDKZO2fnSPCrgM/6WMc7eS20Fbok+0r88fp+YtWVLZrp4CkafFGIp+W0VKw4a22sgebPT99y+FDNMdP4w==", "dependencies": { - "@babel/types": "^7.22.5" + "@babel/types": "^7.22.15" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-module-transforms": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.22.5.tgz", - "integrity": "sha512-+hGKDt/Ze8GFExiVHno/2dvG5IdstpzCq0y4Qc9OJ25D4q3pKfiIP/4Vp3/JvhDkLKsDK2api3q3fpIgiIF5bw==", + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.22.15.tgz", + "integrity": "sha512-l1UiX4UyHSFsYt17iQ3Se5pQQZZHa22zyIXURmvkmLCD4t/aU+dvNWHatKac/D9Vm9UES7nvIqHs4jZqKviUmQ==", "dependencies": { "@babel/helper-environment-visitor": "^7.22.5", - "@babel/helper-module-imports": "^7.22.5", + "@babel/helper-module-imports": "^7.22.15", "@babel/helper-simple-access": "^7.22.5", - "@babel/helper-split-export-declaration": "^7.22.5", - "@babel/helper-validator-identifier": "^7.22.5", - "@babel/template": "^7.22.5", - "@babel/traverse": "^7.22.5", - "@babel/types": "^7.22.5" + "@babel/helper-split-export-declaration": "^7.22.6", + "@babel/helper-validator-identifier": "^7.22.15" }, "engines": { "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" } }, "node_modules/@babel/helper-optimise-call-expression": { @@ -418,17 +482,17 @@ } }, "node_modules/@babel/helper-validator-identifier": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.5.tgz", - "integrity": "sha512-aJXu+6lErq8ltp+JhkJUfk1MTGyuA4v7f3pA+BJ5HLfNC6nAQ0Cpi9uOquUj8Hehg0aUiHzWQbOVJGao6ztBAQ==", + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.15.tgz", + "integrity": "sha512-4E/F9IIEi8WR94324mbDUMo074YTheJmd7eZF5vITTeYchqAi6sYXRLHUVsmkdmY4QjfKTcB2jB7dVP3NaBElQ==", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-validator-option": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.22.5.tgz", - "integrity": "sha512-R3oB6xlIVKUnxNUxbmgq7pKjxpru24zlimpE8WK47fACIlM0II/Hm1RS8IaOI7NgCr6LNS+jl5l75m20npAziw==", + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.22.15.tgz", + "integrity": "sha512-bMn7RmyFjY/mdECUbgn9eoSY4vqvacUnS9i9vGAGttgFWesO6B4CYWA7XlpbWgBt71iv/hfbPlynohStqnu5hA==", "engines": { "node": ">=6.9.0" } @@ -448,25 +512,25 @@ } }, "node_modules/@babel/helpers": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.22.5.tgz", - "integrity": "sha512-pSXRmfE1vzcUIDFQcSGA5Mr+GxBV9oiRKDuDxXvWQQBCh8HoIjs/2DlDB7H8smac1IVrB9/xdXj2N3Wol9Cr+Q==", + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.22.15.tgz", + "integrity": "sha512-7pAjK0aSdxOwR+CcYAqgWOGy5dcfvzsTIfFTb2odQqW47MDfv14UaJDY6eng8ylM2EaeKXdxaSWESbkmaQHTmw==", "dependencies": { - "@babel/template": "^7.22.5", - "@babel/traverse": "^7.22.5", - "@babel/types": "^7.22.5" + "@babel/template": "^7.22.15", + "@babel/traverse": "^7.22.15", + "@babel/types": "^7.22.15" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/highlight": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.22.5.tgz", - "integrity": "sha512-BSKlD1hgnedS5XRnGOljZawtag7H1yPfQp0tdNJCHoH6AZ+Pcm9VvkrK59/Yy593Ypg0zMxH2BxD1VPYUQ7UIw==", + "version": "7.22.13", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.22.13.tgz", + "integrity": "sha512-C/BaXcnnvBCmHTpz/VGZ8jgtE2aYlW4hxDhseJAWZb7gqGM/qtCK6iZUb0TyKFf7BOUsBH7Q7fkRsDRhg1XklQ==", "dependencies": { "@babel/helper-validator-identifier": "^7.22.5", - "chalk": "^2.0.0", + "chalk": "^2.4.2", "js-tokens": "^4.0.0" }, "engines": { @@ -538,9 +602,9 @@ } }, "node_modules/@babel/parser": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.22.5.tgz", - "integrity": "sha512-DFZMC9LJUG9PLOclRC32G63UXwzqS2koQC8dkx+PLdmt1xSePYpbT/NbsrJy8Q/muXz7o/h/d4A7Fuyixm559Q==", + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.22.15.tgz", + "integrity": "sha512-RWmQ/sklUN9BvGGpCDgSubhHWfAx24XDTDObup4ffvxaYsptOg2P3KG0j+1eWKLxpkX0j0uHxmpq2Z1SP/VhxA==", "bin": { "parser": "bin/babel-parser.js" }, @@ -2149,42 +2213,107 @@ } }, "node_modules/@babel/template": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.22.5.tgz", - "integrity": "sha512-X7yV7eiwAxdj9k94NEylvbVHLiVG1nvzCV2EAowhxLTwODV1jl9UzZ48leOC0sH7OnuHrIkllaBgneUykIcZaw==", + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.22.15.tgz", + "integrity": "sha512-QPErUVm4uyJa60rkI73qneDacvdvzxshT3kksGqlGWYdOTIUOwJ7RDUL8sGqslY1uXWSL6xMFKEXDS3ox2uF0w==", "dependencies": { - "@babel/code-frame": "^7.22.5", - "@babel/parser": "^7.22.5", - "@babel/types": "^7.22.5" + "@babel/code-frame": "^7.22.13", + "@babel/parser": "^7.22.15", + "@babel/types": "^7.22.15" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/template/node_modules/@babel/code-frame": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.22.5.tgz", - "integrity": "sha512-Xmwn266vad+6DAqEB2A6V/CcZVp62BbwVmcOJc2RPuwih1kw02TjQvWVWlcKGbBPd+8/0V5DEkOcizRGYsspYQ==", + "version": "7.22.13", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.22.13.tgz", + "integrity": "sha512-XktuhWlJ5g+3TJXc5upd9Ks1HutSArik6jf2eAjYFyIOf4ej3RN+184cZbzDvbPnuTJIUhPKKJE3cIsYTiAT3w==", "dependencies": { - "@babel/highlight": "^7.22.5" + "@babel/highlight": "^7.22.13", + "chalk": "^2.4.2" }, "engines": { "node": ">=6.9.0" } }, + "node_modules/@babel/template/node_modules/ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dependencies": { + "color-convert": "^1.9.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/template/node_modules/chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dependencies": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/template/node_modules/color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/@babel/template/node_modules/color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==" + }, + "node_modules/@babel/template/node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/@babel/template/node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/template/node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/@babel/traverse": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.22.5.tgz", - "integrity": "sha512-7DuIjPgERaNo6r+PZwItpjCZEa5vyw4eJGufeLxrPdBXBoLcCJCIasvK6pK/9DVNrLZTLFhUGqaC6X/PA007TQ==", + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.22.15.tgz", + "integrity": "sha512-DdHPwvJY0sEeN4xJU5uRLmZjgMMDIvMPniLuYzUVXj/GGzysPl0/fwt44JBkyUIzGJPV8QgHMcQdQ34XFuKTYQ==", "dependencies": { - "@babel/code-frame": "^7.22.5", - "@babel/generator": "^7.22.5", + "@babel/code-frame": "^7.22.13", + "@babel/generator": "^7.22.15", "@babel/helper-environment-visitor": "^7.22.5", "@babel/helper-function-name": "^7.22.5", "@babel/helper-hoist-variables": "^7.22.5", - "@babel/helper-split-export-declaration": "^7.22.5", - "@babel/parser": "^7.22.5", - "@babel/types": "^7.22.5", + "@babel/helper-split-export-declaration": "^7.22.6", + "@babel/parser": "^7.22.15", + "@babel/types": "^7.22.15", "debug": "^4.1.0", "globals": "^11.1.0" }, @@ -2193,23 +2322,88 @@ } }, "node_modules/@babel/traverse/node_modules/@babel/code-frame": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.22.5.tgz", - "integrity": "sha512-Xmwn266vad+6DAqEB2A6V/CcZVp62BbwVmcOJc2RPuwih1kw02TjQvWVWlcKGbBPd+8/0V5DEkOcizRGYsspYQ==", + "version": "7.22.13", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.22.13.tgz", + "integrity": "sha512-XktuhWlJ5g+3TJXc5upd9Ks1HutSArik6jf2eAjYFyIOf4ej3RN+184cZbzDvbPnuTJIUhPKKJE3cIsYTiAT3w==", "dependencies": { - "@babel/highlight": "^7.22.5" + "@babel/highlight": "^7.22.13", + "chalk": "^2.4.2" }, "engines": { "node": ">=6.9.0" } }, + "node_modules/@babel/traverse/node_modules/ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dependencies": { + "color-convert": "^1.9.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/traverse/node_modules/chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dependencies": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/traverse/node_modules/color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/@babel/traverse/node_modules/color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==" + }, + "node_modules/@babel/traverse/node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/@babel/traverse/node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/traverse/node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/@babel/types": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.22.5.tgz", - "integrity": "sha512-zo3MIHGOkPOfoRXitsgHLjEXmlDaD/5KU1Uzuc9GNiZPhSqVxVRtxuPaSBZDsYZ9qV88AjtMtWW7ww98loJ9KA==", + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.22.15.tgz", + "integrity": "sha512-X+NLXr0N8XXmN5ZsaQdm9U2SSC3UbIYq/doL++sueHOTisgZHoKaQtZxGuV2cUPQHMfjKEfg/g6oy7Hm6SKFtA==", "dependencies": { "@babel/helper-string-parser": "^7.22.5", - "@babel/helper-validator-identifier": "^7.22.5", + "@babel/helper-validator-identifier": "^7.22.15", "to-fast-properties": "^2.0.0" }, "engines": { @@ -6545,26 +6739,187 @@ } }, "node_modules/babel-jest": { - "version": "28.1.3", - "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-28.1.3.tgz", - "integrity": "sha512-epUaPOEWMk3cWX0M/sPvCHHCe9fMFAa/9hXEgKP8nFfNl/jlGkE9ucq9NqkZGXLDduCJYS0UvSlPUwC0S+rH6Q==", + "version": "29.6.4", + "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-29.6.4.tgz", + "integrity": "sha512-meLj23UlSLddj6PC+YTOFRgDAtjnZom8w/ACsrx0gtPtv5cJZk0A5Unk5bV4wixD7XaPCN1fQvpww8czkZURmw==", "dev": true, "dependencies": { - "@jest/transform": "^28.1.3", + "@jest/transform": "^29.6.4", "@types/babel__core": "^7.1.14", "babel-plugin-istanbul": "^6.1.1", - "babel-preset-jest": "^28.1.3", + "babel-preset-jest": "^29.6.3", "chalk": "^4.0.0", "graceful-fs": "^4.2.9", "slash": "^3.0.0" }, "engines": { - "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" }, "peerDependencies": { "@babel/core": "^7.8.0" } }, + "node_modules/babel-jest/node_modules/@jest/schemas": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", + "integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==", + "dev": true, + "dependencies": { + "@sinclair/typebox": "^0.27.8" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/babel-jest/node_modules/@jest/transform": { + "version": "29.6.4", + "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-29.6.4.tgz", + "integrity": "sha512-8thgRSiXUqtr/pPGY/OsyHuMjGyhVnWrFAwoxmIemlBuiMyU1WFs0tXoNxzcr4A4uErs/ABre76SGmrr5ab/AA==", + "dev": true, + "dependencies": { + "@babel/core": "^7.11.6", + "@jest/types": "^29.6.3", + "@jridgewell/trace-mapping": "^0.3.18", + "babel-plugin-istanbul": "^6.1.1", + "chalk": "^4.0.0", + "convert-source-map": "^2.0.0", + "fast-json-stable-stringify": "^2.1.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.6.4", + "jest-regex-util": "^29.6.3", + "jest-util": "^29.6.3", + "micromatch": "^4.0.4", + "pirates": "^4.0.4", + "slash": "^3.0.0", + "write-file-atomic": "^4.0.2" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/babel-jest/node_modules/@jest/types": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.6.3.tgz", + "integrity": "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==", + "dev": true, + "dependencies": { + "@jest/schemas": "^29.6.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^17.0.8", + "chalk": "^4.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/babel-jest/node_modules/@sinclair/typebox": { + "version": "0.27.8", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", + "integrity": "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==", + "dev": true + }, + "node_modules/babel-jest/node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true + }, + "node_modules/babel-jest/node_modules/jest-haste-map": { + "version": "29.6.4", + "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-29.6.4.tgz", + "integrity": "sha512-12Ad+VNTDHxKf7k+M65sviyynRoZYuL1/GTuhEVb8RYsNSNln71nANRb/faSyWvx0j+gHcivChXHIoMJrGYjog==", + "dev": true, + "dependencies": { + "@jest/types": "^29.6.3", + "@types/graceful-fs": "^4.1.3", + "@types/node": "*", + "anymatch": "^3.0.3", + "fb-watchman": "^2.0.0", + "graceful-fs": "^4.2.9", + "jest-regex-util": "^29.6.3", + "jest-util": "^29.6.3", + "jest-worker": "^29.6.4", + "micromatch": "^4.0.4", + "walker": "^1.0.8" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "optionalDependencies": { + "fsevents": "^2.3.2" + } + }, + "node_modules/babel-jest/node_modules/jest-regex-util": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-29.6.3.tgz", + "integrity": "sha512-KJJBsRCyyLNWCNBOvZyRDnAIfUiRJ8v+hOBQYGn8gDyF3UegwiP4gwRR3/SDa42g1YbVycTidUF3rKjyLFDWbg==", + "dev": true, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/babel-jest/node_modules/jest-util": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.6.3.tgz", + "integrity": "sha512-QUjna/xSy4B32fzcKTSz1w7YYzgiHrjjJjevdRf61HYk998R5vVMMNmrHESYZVDS5DSWs+1srPLPKxXPkeSDOA==", + "dev": true, + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "graceful-fs": "^4.2.9", + "picomatch": "^2.2.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/babel-jest/node_modules/jest-worker": { + "version": "29.6.4", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-29.6.4.tgz", + "integrity": "sha512-6dpvFV4WjcWbDVGgHTWo/aupl8/LbBx2NSKfiwqf79xC/yeJjKHT1+StcKy/2KTmW16hE68ccKVOtXf+WZGz7Q==", + "dev": true, + "dependencies": { + "@types/node": "*", + "jest-util": "^29.6.3", + "merge-stream": "^2.0.0", + "supports-color": "^8.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/babel-jest/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/babel-jest/node_modules/write-file-atomic": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-4.0.2.tgz", + "integrity": "sha512-7KxauUdBmSdWnmpaGFg+ppNjKF8uNLry8LyzjauQDOVONfFLNKrKvQOxZ/VuTIcS/gge/YNahf5RIIQWTSarlg==", + "dev": true, + "dependencies": { + "imurmurhash": "^0.1.4", + "signal-exit": "^3.0.7" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, "node_modules/babel-plugin-istanbul": { "version": "6.1.1", "resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-6.1.1.tgz", @@ -6582,9 +6937,9 @@ } }, "node_modules/babel-plugin-jest-hoist": { - "version": "28.1.3", - "resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-28.1.3.tgz", - "integrity": "sha512-Ys3tUKAmfnkRUpPdpa98eYrAR0nV+sSFUZZEGuQ2EbFd1y4SOLtD5QDNHAq+bb9a+bbXvYQC4b+ID/THIMcU6Q==", + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-29.6.3.tgz", + "integrity": "sha512-ESAc/RJvGTFEzRwOTT4+lNDk/GNHMkKbNzsvT0qKRfDyyYTskxB5rnU2njIDYVxXCBHHEI1c0YwHob3WaYujOg==", "dev": true, "dependencies": { "@babel/template": "^7.3.3", @@ -6593,7 +6948,7 @@ "@types/babel__traverse": "^7.0.6" }, "engines": { - "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, "node_modules/babel-plugin-module-resolver": { @@ -6786,16 +7141,16 @@ } }, "node_modules/babel-preset-jest": { - "version": "28.1.3", - "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-28.1.3.tgz", - "integrity": "sha512-L+fupJvlWAHbQfn74coNX3zf60LXMJsezNvvx8eIh7iOR1luJ1poxYgQk1F8PYtNq/6QODDHCqsSnTFSWC491A==", + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-29.6.3.tgz", + "integrity": "sha512-0B3bhxR6snWXJZtR/RliHTDPRgn1sNHOR0yVtq/IiQFyuOVjFS+wuio/R4gSNkyYmKmJB4wGZv2NZanmKmTnNA==", "dev": true, "dependencies": { - "babel-plugin-jest-hoist": "^28.1.3", + "babel-plugin-jest-hoist": "^29.6.3", "babel-preset-current-node-syntax": "^1.0.0" }, "engines": { - "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" }, "peerDependencies": { "@babel/core": "^7.0.0" @@ -11834,6 +12189,58 @@ } } }, + "node_modules/jest-config/node_modules/babel-jest": { + "version": "28.1.3", + "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-28.1.3.tgz", + "integrity": "sha512-epUaPOEWMk3cWX0M/sPvCHHCe9fMFAa/9hXEgKP8nFfNl/jlGkE9ucq9NqkZGXLDduCJYS0UvSlPUwC0S+rH6Q==", + "dev": true, + "dependencies": { + "@jest/transform": "^28.1.3", + "@types/babel__core": "^7.1.14", + "babel-plugin-istanbul": "^6.1.1", + "babel-preset-jest": "^28.1.3", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "slash": "^3.0.0" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.8.0" + } + }, + "node_modules/jest-config/node_modules/babel-plugin-jest-hoist": { + "version": "28.1.3", + "resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-28.1.3.tgz", + "integrity": "sha512-Ys3tUKAmfnkRUpPdpa98eYrAR0nV+sSFUZZEGuQ2EbFd1y4SOLtD5QDNHAq+bb9a+bbXvYQC4b+ID/THIMcU6Q==", + "dev": true, + "dependencies": { + "@babel/template": "^7.3.3", + "@babel/types": "^7.3.3", + "@types/babel__core": "^7.1.14", + "@types/babel__traverse": "^7.0.6" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" + } + }, + "node_modules/jest-config/node_modules/babel-preset-jest": { + "version": "28.1.3", + "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-28.1.3.tgz", + "integrity": "sha512-L+fupJvlWAHbQfn74coNX3zf60LXMJsezNvvx8eIh7iOR1luJ1poxYgQk1F8PYtNq/6QODDHCqsSnTFSWC491A==", + "dev": true, + "dependencies": { + "babel-plugin-jest-hoist": "^28.1.3", + "babel-preset-current-node-syntax": "^1.0.0" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, "node_modules/jest-diff": { "version": "28.1.3", "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-28.1.3.tgz", diff --git a/package.json b/package.json index 1fc7aa7..accf24f 100644 --- a/package.json +++ b/package.json @@ -25,7 +25,9 @@ "scripts": { "build": "tsc --build ./plugin", "clean": "tsc --build --clean ./plugin", - "test": "jest --config ./plugin/jest.config.js", + "test": "npm run test:plugin & npm run test:src", + "test:plugin": "jest --config ./plugin/jest.config.js", + "test:src": "jest --config jest.config.js --silent", "format": "prettier --write src plugin/src", "lint": "npm run lint:expo & npm run lint:project", "lint:expo": "eslint ./plugin", @@ -47,12 +49,14 @@ } }, "devDependencies": { + "@babel/core": "^7.22.15", "@react-native-community/eslint-config": "^3.2.0", "@react-native/eslint-plugin-specs": "^0.72.4", "@tsconfig/node10": "^1.0.9", "@types/jest": "^28.1.4", "@typescript-eslint/eslint-plugin": "^6.4.0", "@typescript-eslint/parser": "^6.4.0", + "babel-jest": "^29.6.4", "eslint": "^8.47.0", "eslint-config-prettier": "^9.0.0", "eslint-config-universe": "^12.0.0", diff --git a/setupTests.js b/setupTests.js new file mode 100644 index 0000000..5888f18 --- /dev/null +++ b/setupTests.js @@ -0,0 +1,4 @@ +import { NativeModules } from 'react-native'; +import { jest } from '@jest/globals'; + +NativeModules.FullStory = { startPage: jest.fn(), endPage: jest.fn(), updatePage: jest.fn() }; diff --git a/src/FSPage.ts b/src/FSPage.ts index 0b57320..be8f229 100644 --- a/src/FSPage.ts +++ b/src/FSPage.ts @@ -1,21 +1,31 @@ import { NativeModules } from 'react-native'; +// @ts-expect-error const isTurboModuleEnabled = global.__turboModuleProxy != null; +type PropertiesWithoutPageName = { + [key: string]: unknown; +} & { pageName?: never }; + const FullStory = isTurboModuleEnabled ? require('./NativeFullStory').default : NativeModules.FullStory; const { startPage, endPage, updatePage } = FullStory; +type UnknownObj = { + [key: string]: unknown | UnknownObj; +}; + export class FSPage { private pageName: string; private nonce: string; - private properties: Object; + private properties: UnknownObj; - constructor(pageName: string, properties: Object = {}) { + constructor(pageName: string, properties: PropertiesWithoutPageName = {}) { this.pageName = pageName; this.properties = properties; + this.nonce = ''; this.cleanProperties(); } @@ -28,21 +38,21 @@ export class FSPage { }); } - private static isObject(value) { + private static isObject(value: unknown) { return value && typeof value === 'object' && !Array.isArray(value); } - private static merge(oldValue, newValue) { + private static merge(oldValue: unknown, newValue: unknown) { // We can only merge dictionaries and do not perform recursion whenever we // encounter a non-dictionary value // (see com.fullstory.instrumentation FSPageImpl.java) if (!FSPage.isObject(oldValue) || !FSPage.isObject(newValue)) { return newValue; } - return FSPage.mergeObjects(oldValue, newValue); + return FSPage.mergeObjects(oldValue as UnknownObj, newValue as UnknownObj); } - private static mergeObjects(oldObj, newObj) { + private static mergeObjects(oldObj: UnknownObj, newObj: UnknownObj) { // return new object instance on immutable "old" object frozen by RN const mergedObj = { ...oldObj }; for (const key in newObj) { @@ -63,7 +73,7 @@ export class FSPage { } } - update(properties: Object) { + update(properties: PropertiesWithoutPageName) { if (!this.nonce) { console.error( 'Called `updateProperties` on FSPage that has not been `start`-ed. This may ' + @@ -73,14 +83,14 @@ export class FSPage { return; } - this.properties = FSPage.merge(this.properties, properties); + this.properties = FSPage.merge(this.properties, properties) as UnknownObj; this.cleanProperties(); updatePage(this.nonce, this.properties); } - start(properties?: Object) { + start(properties?: PropertiesWithoutPageName) { if (properties) { - this.properties = FSPage.merge(this.properties, properties); + this.properties = FSPage.merge(this.properties, properties) as UnknownObj; this.cleanProperties(); } this.nonce = FSPage.generateUUID(); diff --git a/src/__tests__/FSPage.test.ts b/src/__tests__/FSPage.test.ts new file mode 100644 index 0000000..efba63d --- /dev/null +++ b/src/__tests__/FSPage.test.ts @@ -0,0 +1,104 @@ +import { describe, it, expect, afterEach } from '@jest/globals'; +import { FSPage } from '../FSPage'; +import { NativeModules } from 'react-native'; + +describe('FSPage', () => { + const SAMPLE_PAGE_NAME = 'sample page name'; + const SAMPLE_PAGE_PROPS = { + name: 'bob', + zip: 'twelve', + baz: { foo: { bar: 123 } }, + bat: { address: 'mark' }, + }; + const SAMPLE_UPDATED_PAGE_PROPS = { + name: 'bob', + zip: 12, + baz: { foo: { hat: 456 }, dar: 'dup' }, + bat: true, + code: 'tree', + }; + const PAGE_PROPS_WITH_PAGE_NAME = { ...SAMPLE_PAGE_PROPS, pageName: 'test' }; + + afterEach(() => { + jest.clearAllMocks(); + }); + + it('Page start calls startPage with correct arguments', () => { + const page = new FSPage(SAMPLE_PAGE_NAME, SAMPLE_PAGE_PROPS); + + page.start(); + + const startPage = NativeModules.FullStory.startPage; + expect(startPage).toBeCalledTimes(1); + // @ts-expect-error nonce is a private property + expect(startPage).toBeCalledWith(page.nonce, SAMPLE_PAGE_NAME, SAMPLE_PAGE_PROPS); + }); + + it('Page update calls updatePage with correct arguments', () => { + const page = new FSPage(SAMPLE_PAGE_NAME, SAMPLE_PAGE_PROPS); + + page.start(); + page.update(SAMPLE_PAGE_PROPS); + + const updatePage = NativeModules.FullStory.updatePage; + expect(updatePage).toBeCalledTimes(1); + // @ts-expect-error nonce is a private property + expect(updatePage).toBeCalledWith(page.nonce, SAMPLE_PAGE_PROPS); + }); + + it('Page end calls endPage with correct arguments', () => { + const page = new FSPage(SAMPLE_PAGE_NAME, SAMPLE_PAGE_PROPS); + + page.start(); + // @ts-expect-error nonce is a private property + const nonceCopy = `${page.nonce}`; + page.end(); + + const endPage = NativeModules.FullStory.endPage; + expect(endPage).toBeCalledTimes(1); + expect(endPage).toBeCalledWith(nonceCopy); + }); + + it('Page start will remove pageName key', () => { + const page = new FSPage(SAMPLE_PAGE_NAME); + + // @ts-expect-error pageName is not allowed + page.start(PAGE_PROPS_WITH_PAGE_NAME); + + const startPage = NativeModules.FullStory.startPage; + // @ts-expect-error nonce is a private property + expect(startPage).toBeCalledWith(page.nonce, SAMPLE_PAGE_NAME, SAMPLE_PAGE_PROPS); + }); + + it('Unstarted page will not call update or end page', () => { + const page = new FSPage(SAMPLE_PAGE_NAME); + page.update(SAMPLE_PAGE_PROPS); + page.end(); + + const updatePage = NativeModules.FullStory.updatePage; + const endPage = NativeModules.FullStory.endPage; + + expect(updatePage).not.toHaveBeenCalled(); + expect(endPage).not.toHaveBeenCalled(); + }); + + it('Page update will merge properties correctly ', () => { + const page = new FSPage(SAMPLE_PAGE_NAME); + + page.start(SAMPLE_PAGE_PROPS); + page.update(SAMPLE_UPDATED_PAGE_PROPS); + + const updatePage = NativeModules.FullStory.updatePage; + + const expectedMergedObj = { + name: 'bob', + zip: 12, + baz: { foo: { bar: 123, hat: 456 }, dar: 'dup' }, + bat: true, + code: 'tree', + }; + + // @ts-expect-error nonce is a private property + expect(updatePage).toHaveBeenCalledWith(page.nonce, expectedMergedObj); + }); +}); From b44fd570d0133b93d1d1e80977166ffeb73dba3e Mon Sep 17 00:00:00 2001 From: Ryan Wang Date: Fri, 8 Sep 2023 16:19:43 -0400 Subject: [PATCH 7/8] use uppercase UUIDs, exclude lib/ from jest --- jest.config.js | 2 +- src/FSPage.ts | 2 +- tsconfig.json | 3 ++- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/jest.config.js b/jest.config.js index 0cc6cb8..0ffa5fe 100644 --- a/jest.config.js +++ b/jest.config.js @@ -8,5 +8,5 @@ module.exports = { '\\.(ts|tsx)$': 'ts-jest', }, transformIgnorePatterns: ['/node_modules/(?!(@react-native|react-native)/).*/'], - testPathIgnorePatterns: ['/node_modules/', '/plugin'], + testPathIgnorePatterns: ['/node_modules/', '/plugin', '/lib'], }; diff --git a/src/FSPage.ts b/src/FSPage.ts index be8f229..130005c 100644 --- a/src/FSPage.ts +++ b/src/FSPage.ts @@ -34,7 +34,7 @@ export class FSPage { private static generateUUID() { return 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx'.replace(/[x]/g, () => { const char = Math.floor(Math.random() * 16); - return char.toString(16); + return char.toString(16).toUpperCase(); }); } diff --git a/tsconfig.json b/tsconfig.json index 6c848f7..113f224 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -13,5 +13,6 @@ "module": "CommonJS", "types": ["react-native", "jest"] }, - "include": ["src"] + "include": ["src"], + "exclude": ["src/**/*.test.ts"] } From 555a31221882b789d6050bb16e466b18a3a1302c Mon Sep 17 00:00:00 2001 From: Ryan Wang Date: Mon, 11 Sep 2023 10:31:55 -0400 Subject: [PATCH 8/8] implement new uuid generator, update tests --- src/FSPage.ts | 10 ++----- src/__tests__/FSPage.test.ts | 23 ++++++++-------- src/utils.ts | 53 ++++++++++++++++++++++++++++++++++++ 3 files changed, 67 insertions(+), 19 deletions(-) create mode 100644 src/utils.ts diff --git a/src/FSPage.ts b/src/FSPage.ts index 130005c..220b2c8 100644 --- a/src/FSPage.ts +++ b/src/FSPage.ts @@ -1,4 +1,5 @@ import { NativeModules } from 'react-native'; +import { generateUUID } from './utils'; // @ts-expect-error const isTurboModuleEnabled = global.__turboModuleProxy != null; @@ -31,13 +32,6 @@ export class FSPage { private static FS_PAGE_NAME_KEY = 'pageName'; - private static generateUUID() { - return 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx'.replace(/[x]/g, () => { - const char = Math.floor(Math.random() * 16); - return char.toString(16).toUpperCase(); - }); - } - private static isObject(value: unknown) { return value && typeof value === 'object' && !Array.isArray(value); } @@ -93,7 +87,7 @@ export class FSPage { this.properties = FSPage.merge(this.properties, properties) as UnknownObj; this.cleanProperties(); } - this.nonce = FSPage.generateUUID(); + this.nonce = generateUUID(); startPage(this.nonce, this.pageName, this.properties); } diff --git a/src/__tests__/FSPage.test.ts b/src/__tests__/FSPage.test.ts index efba63d..c1fa246 100644 --- a/src/__tests__/FSPage.test.ts +++ b/src/__tests__/FSPage.test.ts @@ -2,6 +2,12 @@ import { describe, it, expect, afterEach } from '@jest/globals'; import { FSPage } from '../FSPage'; import { NativeModules } from 'react-native'; +const SAMPLE_UUID = '64CB2E82-3002-E01B-B58A-E089B79F6223'; + +jest.mock('../utils', () => ({ + generateUUID: jest.fn().mockImplementation(() => SAMPLE_UUID), +})); + describe('FSPage', () => { const SAMPLE_PAGE_NAME = 'sample page name'; const SAMPLE_PAGE_PROPS = { @@ -30,8 +36,7 @@ describe('FSPage', () => { const startPage = NativeModules.FullStory.startPage; expect(startPage).toBeCalledTimes(1); - // @ts-expect-error nonce is a private property - expect(startPage).toBeCalledWith(page.nonce, SAMPLE_PAGE_NAME, SAMPLE_PAGE_PROPS); + expect(startPage).toBeCalledWith(SAMPLE_UUID, SAMPLE_PAGE_NAME, SAMPLE_PAGE_PROPS); }); it('Page update calls updatePage with correct arguments', () => { @@ -42,21 +47,18 @@ describe('FSPage', () => { const updatePage = NativeModules.FullStory.updatePage; expect(updatePage).toBeCalledTimes(1); - // @ts-expect-error nonce is a private property - expect(updatePage).toBeCalledWith(page.nonce, SAMPLE_PAGE_PROPS); + expect(updatePage).toBeCalledWith(SAMPLE_UUID, SAMPLE_PAGE_PROPS); }); it('Page end calls endPage with correct arguments', () => { const page = new FSPage(SAMPLE_PAGE_NAME, SAMPLE_PAGE_PROPS); page.start(); - // @ts-expect-error nonce is a private property - const nonceCopy = `${page.nonce}`; page.end(); const endPage = NativeModules.FullStory.endPage; expect(endPage).toBeCalledTimes(1); - expect(endPage).toBeCalledWith(nonceCopy); + expect(endPage).toBeCalledWith(SAMPLE_UUID); }); it('Page start will remove pageName key', () => { @@ -66,8 +68,8 @@ describe('FSPage', () => { page.start(PAGE_PROPS_WITH_PAGE_NAME); const startPage = NativeModules.FullStory.startPage; - // @ts-expect-error nonce is a private property - expect(startPage).toBeCalledWith(page.nonce, SAMPLE_PAGE_NAME, SAMPLE_PAGE_PROPS); + + expect(startPage).toBeCalledWith(SAMPLE_UUID, SAMPLE_PAGE_NAME, SAMPLE_PAGE_PROPS); }); it('Unstarted page will not call update or end page', () => { @@ -98,7 +100,6 @@ describe('FSPage', () => { code: 'tree', }; - // @ts-expect-error nonce is a private property - expect(updatePage).toHaveBeenCalledWith(page.nonce, expectedMergedObj); + expect(updatePage).toHaveBeenCalledWith(SAMPLE_UUID, expectedMergedObj); }); }); diff --git a/src/utils.ts b/src/utils.ts new file mode 100644 index 0000000..112fa5a --- /dev/null +++ b/src/utils.ts @@ -0,0 +1,53 @@ +/* eslint-disable no-bitwise */ + +export const generateUUID = (function () { + function hex8(n: number) { + return ((n >>> 0) + 4294967296).toString(16).substring(1).toUpperCase(); + } + + // state (a is always initialized to odd value, so state is never 0) + var a = (Math.random() * 4294967296) | 1, + b = (Math.random() * 4294967296) | 0, + c = (Date.now() / 4294967296) ^ (Math.random() * 4294967296), + d = Date.now() ^ (Math.random() * 4294967296); + + // four applications of xorshift128 + var f = function () { + var t = d | 0; + t ^= t << 11; + t ^= t >>> 8; + d = t ^ a ^ (a >>> 19); + t = c | 0; + t ^= t << 11; + t ^= t >>> 8; + c = t ^ d ^ (d >>> 19); + t = b | 0; + t ^= t << 11; + t ^= t >>> 8; + b = t ^ c ^ (c >>> 19); + t = a | 0; + t ^= t << 11; + t ^= t >>> 8; + a = t ^ b ^ (b >>> 19); + return ( + hex8(a) + + '-' + + hex8(b).substring(0, 4) + + '-' + + hex8(b).substring(4) + + '-' + + hex8(c).substring(0, 4) + + '-' + + hex8(c).substring(4) + + hex8(d) + ); + }; + + // mix initial state - not strictly necessary + f(); + f(); + f(); + f(); + + return f; +})();