diff --git a/android/src/legacy/java/com/FullStoryModule.java b/android/src/legacy/java/com/FullStoryModule.java index 617a5bd..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 { @@ -86,4 +76,19 @@ public static void log(double level, String message) { public static void resetIdleTimer() { FullStoryModuleImpl.resetIdleTimer(); } + + @ReactMethod + public static void startPage(String nonce, String pageName, ReadableMap pageProperties) { + FullStoryModuleImpl.startPage(nonce, pageName, pageProperties); + } + + @ReactMethod + public static void updatePage(String uuid, ReadableMap pageProperties) { + FullStoryModuleImpl.updatePage(uuid, pageProperties); + } + + @ReactMethod + 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 118a6f7..8f577b2 100644 --- a/android/src/main/java/com/fullstory/reactnative/FullStoryModuleImpl.java +++ b/android/src/main/java/com/fullstory/reactnative/FullStoryModuleImpl.java @@ -1,5 +1,7 @@ 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; @@ -8,12 +10,42 @@ 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 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.41; 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(); @@ -137,4 +169,45 @@ private static Map toMap(ReadableMap map) { return map.toHashMap(); } + + public static void startPage(String uuid, String pageName, ReadableMap pageProperties) { + if (!reflectionSuccess) { + return; + } + + 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(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(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 44a0254..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 { @@ -87,4 +76,19 @@ public void log(double level, String message) { public void resetIdleTimer() { FullStoryModuleImpl.resetIdleTimer(); } + + @Override + public void startPage(String nonce, String pageName, ReadableMap pageProperties) { + FullStoryModuleImpl.startPage(nonce, pageName, pageProperties); + } + + @Override + public void updatePage(String uuid, ReadableMap pageProperties) { + FullStoryModuleImpl.updatePage(uuid, pageProperties); + } + + @Override + public void endPage(String uuid) { + FullStoryModuleImpl.endPage(uuid); + } } 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/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 1810c46..bd69b92 100644 --- a/ios/FullStory.mm +++ b/ios/FullStory.mm @@ -5,6 +5,7 @@ #import #import #import +#import #import "FSReactSwizzle.h" @@ -12,6 +13,8 @@ @implementation FullStory { RCTPromiseResolveBlock onReadyPromise; } +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) @@ -128,6 +131,45 @@ - (void) getCurrentSessionURL:(RCTPromiseResolveBlock)resolve reject:(RCTPromise }); } +RCT_EXPORT_METHOD(startPage:(NSString *)nonce pageName:(NSString *)pageName pageProperties:(NSDictionary *)pageProperties) +{ + if (![FS respondsToSelector:@selector(_pageViewWithNonce:name:properties:)]) { + RCTLogError(PagesAPIError, @"startPage"); + } else { + NSUUID *uuid = [[NSUUID alloc] initWithUUIDString:nonce]; + + dispatch_async(dispatch_get_main_queue(), ^{ + [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(), ^{ + [FS _endPageWithNonce:uuid]; + }); + } +} + +RCT_EXPORT_METHOD(updatePage:(NSString *)nonce pageProperties:(NSDictionary *)pageProperties) +{ + if (![FS respondsToSelector:@selector(_updatePageWithNonce:properties:)]) { + RCTLogError(PagesAPIError, @"updatePage"); + } else { + NSUUID *uuid = [[NSUUID alloc] initWithUUIDString:nonce]; + + dispatch_async(dispatch_get_main_queue(), ^{ + [FS _updatePageWithNonce:uuid properties:pageProperties]; + }); + } +} + - (void) fullstoryDidStartSession:(NSString *)sessionUrl { if (!onReadyPromise) return; diff --git a/jest.config.js b/jest.config.js new file mode 100644 index 0000000..0ffa5fe --- /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', '/lib'], +}; 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 new file mode 100644 index 0000000..220b2c8 --- /dev/null +++ b/src/FSPage.ts @@ -0,0 +1,106 @@ +import { NativeModules } from 'react-native'; +import { generateUUID } from './utils'; + +// @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: UnknownObj; + + constructor(pageName: string, properties: PropertiesWithoutPageName = {}) { + this.pageName = pageName; + this.properties = properties; + this.nonce = ''; + this.cleanProperties(); + } + + private static FS_PAGE_NAME_KEY = 'pageName'; + + private static isObject(value: unknown) { + return value && typeof value === 'object' && !Array.isArray(value); + } + + 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 as UnknownObj, newValue as UnknownObj); + } + + 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) { + 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: PropertiesWithoutPageName) { + 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) as UnknownObj; + this.cleanProperties(); + updatePage(this.nonce, this.properties); + } + + start(properties?: PropertiesWithoutPageName) { + if (properties) { + this.properties = FSPage.merge(this.properties, properties) as UnknownObj; + this.cleanProperties(); + } + this.nonce = 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 580c973..bb380a4 100644 --- a/src/NativeFullStory.ts +++ b/src/NativeFullStory.ts @@ -15,6 +15,9 @@ export interface Spec extends TurboModule { restart(): void; log(logLevel: number, message: string): void; resetIdleTimer(): 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/__tests__/FSPage.test.ts b/src/__tests__/FSPage.test.ts new file mode 100644 index 0000000..c1fa246 --- /dev/null +++ b/src/__tests__/FSPage.test.ts @@ -0,0 +1,105 @@ +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 = { + 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); + expect(startPage).toBeCalledWith(SAMPLE_UUID, 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); + 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(); + page.end(); + + const endPage = NativeModules.FullStory.endPage; + expect(endPage).toBeCalledTimes(1); + expect(endPage).toBeCalledWith(SAMPLE_UUID); + }); + + 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; + + expect(startPage).toBeCalledWith(SAMPLE_UUID, 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', + }; + + expect(updatePage).toHaveBeenCalledWith(SAMPLE_UUID, expectedMergedObj); + }); +}); diff --git a/src/index.ts b/src/index.ts index 3bf5769..fa3acf9 100644 --- a/src/index.ts +++ b/src/index.ts @@ -71,6 +71,8 @@ declare global { const identifyWithProperties = (uid: string, userVars = {}) => identify(uid, userVars); +export { FSPage } from './FSPage'; + const FullStoryAPI: FullStoryStatic = { anonymize, identify: identifyWithProperties, 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; +})(); 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"] }