diff --git a/android/app/build.gradle b/android/app/build.gradle index 868ab592b63d..4cb3ea6c8837 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -2,12 +2,21 @@ apply plugin: "com.android.application" apply plugin: "org.jetbrains.kotlin.android" apply plugin: "com.facebook.react" apply plugin: "com.google.firebase.firebase-perf" +apply plugin: "fullstory" apply from: project(':react-native-config').projectDir.getPath() + "/dotenv.gradle" /** * This is the configuration block to customize your React Native Android app. * By default you don't need to apply any configuration, just uncomment the lines you need. */ + +/* Fullstory settings */ +fullstory { + org 'o-1WN56P-na1' + enabledVariants 'all' + logcatLevel 'debug' +} + react { /* Folders */ // The root of your project, i.e. where "package.json" lives. Default is '..' @@ -162,7 +171,7 @@ android { signingConfig null // buildTypes take precedence over productFlavors when it comes to the signing configuration, // thus we need to manually set the signing config, so that the e2e uses the debug config again. - // In other words, the signingConfig setting above will be ignored when we build the flavor in release mode. + // In other words, the signingConfig setting above will be ignored when we build the flavor in release mode. productFlavors.all { flavor -> // All release builds should be signed with the release config ... flavor.signingConfig signingConfigs.release diff --git a/android/build.gradle b/android/build.gradle index 10600480d8bb..52c998998ba0 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -20,6 +20,7 @@ buildscript { repositories { google() mavenCentral() + maven {url "https://maven.fullstory.com"} } dependencies { classpath("com.android.tools.build:gradle") @@ -27,6 +28,9 @@ buildscript { classpath("com.google.gms:google-services:4.3.4") classpath("com.google.firebase:firebase-crashlytics-gradle:2.7.1") classpath("com.google.firebase:perf-plugin:1.4.1") + // Fullstory integration + classpath ("com.fullstory:gradle-plugin-local:1.47.0") + // NOTE: Do not place your application dependencies here; they belong // in the individual module build.gradle files classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion") @@ -70,7 +74,7 @@ allprojects { // 'mapbox' is the fixed username for Mapbox's Maven repository. username = 'mapbox' - // The value for password is read from the 'MAPBOX_DOWNLOADS_TOKEN' gradle property. + // The value for password is read from the 'MAPBOX_DOWNLOADS_TOKEN' gradle property. // Run "npm run setup-mapbox-sdk" to set this property in «USER_HOME»/.gradle/gradle.properties // Example gradle.properties entry: diff --git a/babel.config.js b/babel.config.js index 9f8b7a711d78..3023d37df7e0 100644 --- a/babel.config.js +++ b/babel.config.js @@ -35,6 +35,17 @@ const metro = { ['@babel/plugin-proposal-private-property-in-object', {loose: true}], // The reanimated babel plugin needs to be last, as stated here: https://docs.swmansion.com/react-native-reanimated/docs/fundamentals/installation 'react-native-reanimated/plugin', + + /* Fullstory */ + '@fullstory/react-native', + [ + '@fullstory/babel-plugin-annotate-react', + { + native: true, + setFSTagName: true, + }, + ], + // Import alias for native devices [ 'module-resolver', diff --git a/config/webpack/webpack.common.ts b/config/webpack/webpack.common.ts index 7cafafca9973..9d397b9557a3 100644 --- a/config/webpack/webpack.common.ts +++ b/config/webpack/webpack.common.ts @@ -98,7 +98,7 @@ const getCommonConfiguration = ({file = '.env', platform = 'web'}: Environment): {from: 'web/apple-touch-icon.png'}, {from: 'assets/images/expensify-app-icon.svg'}, {from: 'web/manifest.json'}, - {from: 'web/gtm.js'}, + {from: 'web/thirdPartyScripts.js'}, {from: 'assets/css', to: 'css'}, {from: 'assets/fonts/web', to: 'fonts'}, {from: 'assets/sounds', to: 'sounds'}, diff --git a/ios/NewExpensify.xcodeproj/project.pbxproj b/ios/NewExpensify.xcodeproj/project.pbxproj index 94118eb3bfa4..9e4fcaef55dd 100644 --- a/ios/NewExpensify.xcodeproj/project.pbxproj +++ b/ios/NewExpensify.xcodeproj/project.pbxproj @@ -648,6 +648,7 @@ "${PODS_ROOT}/Target Support Files/Pods-NewExpensify/Pods-NewExpensify-frameworks.sh", "${BUILT_PRODUCTS_DIR}/MapboxMaps/MapboxMaps.framework", "${BUILT_PRODUCTS_DIR}/Turf/Turf.framework", + "${PODS_XCFRAMEWORKS_BUILD_DIR}/FullStory/FullStory.framework/FullStory", "${PODS_XCFRAMEWORKS_BUILD_DIR}/MapboxCommon/MapboxCommon.framework/MapboxCommon", "${PODS_XCFRAMEWORKS_BUILD_DIR}/MapboxCoreMaps/MapboxCoreMaps.framework/MapboxCoreMaps", "${PODS_XCFRAMEWORKS_BUILD_DIR}/MapboxMobileEvents/MapboxMobileEvents.framework/MapboxMobileEvents", @@ -659,6 +660,7 @@ outputPaths = ( "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/MapboxMaps.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Turf.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/FullStory.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/MapboxCommon.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/MapboxCoreMaps.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/MapboxMobileEvents.framework", @@ -680,6 +682,7 @@ "${PODS_ROOT}/Target Support Files/Pods-NewExpensify-NewExpensifyTests/Pods-NewExpensify-NewExpensifyTests-frameworks.sh", "${BUILT_PRODUCTS_DIR}/MapboxMaps/MapboxMaps.framework", "${BUILT_PRODUCTS_DIR}/Turf/Turf.framework", + "${PODS_XCFRAMEWORKS_BUILD_DIR}/FullStory/FullStory.framework/FullStory", "${PODS_XCFRAMEWORKS_BUILD_DIR}/MapboxCommon/MapboxCommon.framework/MapboxCommon", "${PODS_XCFRAMEWORKS_BUILD_DIR}/MapboxCoreMaps/MapboxCoreMaps.framework/MapboxCoreMaps", "${PODS_XCFRAMEWORKS_BUILD_DIR}/MapboxMobileEvents/MapboxMobileEvents.framework/MapboxMobileEvents", @@ -691,6 +694,7 @@ outputPaths = ( "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/MapboxMaps.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Turf.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/FullStory.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/MapboxCommon.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/MapboxCoreMaps.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/MapboxMobileEvents.framework", diff --git a/ios/Podfile b/ios/Podfile index 4f00eb2adfdd..d72086d4c07b 100644 --- a/ios/Podfile +++ b/ios/Podfile @@ -117,3 +117,5 @@ end target 'NotificationServiceExtension' do pod 'AirshipServiceExtension' end + +pod 'FullStory', :http => 'https://ios-releases.fullstory.com/fullstory-1.48.0-xcframework.tar.gz' \ No newline at end of file diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 911d29e0ab78..1ea6b65a58b7 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -138,6 +138,27 @@ PODS: - GoogleUtilities/Environment (~> 7.7) - "GoogleUtilities/NSData+zlib (~> 7.7)" - fmt (6.2.1) + - FullStory (1.48.0) + - fullstory_react-native (1.4.2): + - FullStory (~> 1.14) + - glog + - hermes-engine + - RCT-Folly (= 2022.05.16.00) + - RCTRequired + - RCTTypeSafety + - React-Codegen + - React-Core + - React-debug + - React-Fabric + - React-graphics + - React-ImageManager + - React-NativeModulesApple + - React-RCTFabric + - React-rendererdebug + - React-utils + - ReactCommon/turbomodule/bridging + - ReactCommon/turbomodule/core + - Yoga - glog (0.3.5) - GoogleAppMeasurement (8.8.0): - GoogleAppMeasurement/AdIdSupport (= 8.8.0) @@ -2077,6 +2098,8 @@ DEPENDENCIES: - ExpoImageManipulator (from `../node_modules/expo-image-manipulator/ios`) - ExpoModulesCore (from `../node_modules/expo-modules-core`) - FBLazyVector (from `../node_modules/react-native/Libraries/FBLazyVector`) + - "FullStory (from `{:http=>\"https://ios-releases.fullstory.com/fullstory-1.48.0-xcframework.tar.gz\"}`)" + - "fullstory_react-native (from `../node_modules/@fullstory/react-native`)" - glog (from `../node_modules/react-native/third-party-podspecs/glog.podspec`) - hermes-engine (from `../node_modules/react-native/sdks/hermes-engine/hermes-engine.podspec`) - libevent (~> 2.1.12) @@ -2239,6 +2262,10 @@ EXTERNAL SOURCES: :path: "../node_modules/expo-modules-core" FBLazyVector: :path: "../node_modules/react-native/Libraries/FBLazyVector" + FullStory: + :http: https://ios-releases.fullstory.com/fullstory-1.48.0-xcframework.tar.gz + fullstory_react-native: + :path: "../node_modules/@fullstory/react-native" glog: :podspec: "../node_modules/react-native/third-party-podspecs/glog.podspec" hermes-engine: @@ -2429,6 +2456,10 @@ EXTERNAL SOURCES: Yoga: :path: "../node_modules/react-native/ReactCommon/yoga" +CHECKOUT OPTIONS: + FullStory: + :http: https://ios-releases.fullstory.com/fullstory-1.48.0-xcframework.tar.gz + SPEC CHECKSUMS: Airship: 5a6d3f8a982398940b0d48423bb9b8736717c123 AirshipFrameworkProxy: 7255f4ed9836dc2920f2f1ea5657ced4cee8a35c @@ -2454,6 +2485,8 @@ SPEC CHECKSUMS: FirebasePerformance: 0c01a7a496657d7cea86d40c0b1725259d164c6c FirebaseRemoteConfig: 2d6e2cfdb49af79535c8af8a80a4a5009038ec2b fmt: ff9d55029c625d3757ed641535fd4a75fedc7ce9 + FullStory: 097347c823c21c655ca25fd8d5e6355a9326ec54 + fullstory_react-native: 6cba8a2c054374a24a44dc4310407d9435459cae glog: c5d68082e772fa1c511173d6b30a9de2c05a69a2 GoogleAppMeasurement: 5ba1164e3c844ba84272555e916d0a6d3d977e91 GoogleDataTransport: 6c09b596d841063d76d4288cc2d2f42cc36e1e2a @@ -2573,8 +2606,8 @@ SPEC CHECKSUMS: SocketRocket: f32cd54efbe0f095c4d7594881e52619cfe80b17 Turf: 13d1a92d969ca0311bbc26e8356cca178ce95da2 VisionCamera: 1394a316c7add37e619c48d7aa40b38b954bf055 - Yoga: 1b901a6d6eeba4e8a2e8f308f708691cdb5db312 + Yoga: 64cd2a583ead952b0315d5135bf39e053ae9be70 -PODFILE CHECKSUM: a25a81f2b50270f0c0bd0aff2e2ebe4d0b4ec06d +PODFILE CHECKSUM: 66a5c97ae1059e4da1993a4ad95abe5d819f555b COCOAPODS: 1.13.0 diff --git a/jest/setup.ts b/jest/setup.ts index 488e3e36a1d3..174e59a7e493 100644 --- a/jest/setup.ts +++ b/jest/setup.ts @@ -2,9 +2,11 @@ import '@shopify/flash-list/jestSetup'; import 'react-native-gesture-handler/jestSetup'; import mockStorage from 'react-native-onyx/dist/storage/__mocks__'; import 'setimmediate'; +import mockFSLibrary from './setupMockFullstoryLib'; import setupMockImages from './setupMockImages'; setupMockImages(); +mockFSLibrary(); // This mock is required as per setup instructions for react-navigation testing // https://reactnavigation.org/docs/testing/#mocking-native-modules diff --git a/jest/setupMockFullstoryLib.ts b/jest/setupMockFullstoryLib.ts new file mode 100644 index 000000000000..9edfccab9441 --- /dev/null +++ b/jest/setupMockFullstoryLib.ts @@ -0,0 +1,24 @@ +type FSPageInterface = { + start: jest.Mock; +}; + +export default function mockFSLibrary() { + jest.mock('@fullstory/react-native', () => { + class Fullstory { + consent = jest.fn(); + + anonymize = jest.fn(); + + identify = jest.fn(); + } + + return { + FSPage(): FSPageInterface { + return { + start: jest.fn(), + }; + }, + default: Fullstory, + }; + }); +} diff --git a/package-lock.json b/package-lock.json index eb28cb305e45..bad61d4704bc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -20,6 +20,9 @@ "@formatjs/intl-locale": "^3.3.0", "@formatjs/intl-numberformat": "^8.5.0", "@formatjs/intl-pluralrules": "^5.2.2", + "@fullstory/babel-plugin-react-native": "^1.2.1", + "@fullstory/browser": "^2.0.3", + "@fullstory/react-native": "^1.4.2", "@gorhom/portal": "^1.0.14", "@invertase/react-native-apple-authentication": "^2.2.2", "@kie/act-js": "^2.6.0", @@ -5589,6 +5592,52 @@ "tslib": "^2.4.0" } }, + "node_modules/@fullstory/babel-plugin-annotate-react": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@fullstory/babel-plugin-annotate-react/-/babel-plugin-annotate-react-2.3.0.tgz", + "integrity": "sha512-gYLUL6Tu0exbvTIhK9nSCaztmqBlQAm07Fvtl/nKTc+lxwFkcX9vR8RrdTbyjJZKbPaA5EMlExQ6GeLCXkfm5g==" + }, + "node_modules/@fullstory/babel-plugin-react-native": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@fullstory/babel-plugin-react-native/-/babel-plugin-react-native-1.2.1.tgz", + "integrity": "sha512-EMAgoPOo+31eppHxQf05oAGhKKTem7rw8GHDdbNJF0c5dQWzBGNVgF72TPjcxES91UI6hbss2eqoVOhUttLEoQ==", + "dependencies": { + "@babel/parser": "^7.0.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@fullstory/browser": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@fullstory/browser/-/browser-2.0.3.tgz", + "integrity": "sha512-usjH8FB1O2LiSWoblsuKhFhlYDGpIPuyQVOx4JXtxm9QmQARdKZdNq1vPijxuDvOGjhwtVZa4JmhvByRRuDPnQ==", + "dependencies": { + "@fullstory/snippet": "2.0.3" + } + }, + "node_modules/@fullstory/react-native": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/@fullstory/react-native/-/react-native-1.4.2.tgz", + "integrity": "sha512-Ig85ghn5UN+Tc1JWL/y4hY9vleeaVHL3f6qH9W4odDNP4XAv29+G82nIYQhBOQGoVnIQ4oQFQftir/dqAbidSw==", + "dependencies": { + "@fullstory/babel-plugin-annotate-react": "^2.2.0", + "@fullstory/babel-plugin-react-native": "^1.1.0" + }, + "peerDependencies": { + "expo": ">=47.0.0", + "react": "*", + "react-native": ">=0.61.0" + }, + "peerDependenciesMeta": { + "expo": { + "optional": true + } + } + }, + "node_modules/@fullstory/snippet": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@fullstory/snippet/-/snippet-2.0.3.tgz", + "integrity": "sha512-EaCuTQSLv5FvnjHLbTxErn3sS1+nLqf1p6sA/c4PV49stBtkUakA0eLhJJdaw0WLdXyEzZXf86lRNsjEzrgGPw==" + }, "node_modules/@gar/promisify": { "version": "1.1.3", "license": "MIT" diff --git a/package.json b/package.json index c00f7bd1c436..6f40533fea37 100644 --- a/package.json +++ b/package.json @@ -72,6 +72,9 @@ "@formatjs/intl-locale": "^3.3.0", "@formatjs/intl-numberformat": "^8.5.0", "@formatjs/intl-pluralrules": "^5.2.2", + "@fullstory/babel-plugin-react-native": "^1.2.1", + "@fullstory/browser": "^2.0.3", + "@fullstory/react-native": "^1.4.2", "@gorhom/portal": "^1.0.14", "@invertase/react-native-apple-authentication": "^2.2.2", "@kie/act-js": "^2.6.0", diff --git a/src/ONYXKEYS.ts b/src/ONYXKEYS.ts index ddf37fba2354..e669d4740f98 100755 --- a/src/ONYXKEYS.ts +++ b/src/ONYXKEYS.ts @@ -188,6 +188,9 @@ const ONYXKEYS = { /** User's Expensify Wallet */ USER_WALLET: 'userWallet', + /** User's metadata that will be used to segmentation */ + USER_METADATA: 'userMetadata', + /** Object containing Onfido SDK Token + applicantID */ WALLET_ONFIDO: 'walletOnfido', @@ -610,6 +613,7 @@ type OnyxValuesMapping = { [ONYXKEYS.USER_LOCATION]: OnyxTypes.UserLocation; [ONYXKEYS.LOGIN_LIST]: OnyxTypes.LoginList; [ONYXKEYS.SESSION]: OnyxTypes.Session; + [ONYXKEYS.USER_METADATA]: OnyxTypes.UserMetadata; [ONYXKEYS.STASHED_SESSION]: OnyxTypes.Session; [ONYXKEYS.BETAS]: OnyxTypes.Beta[]; [ONYXKEYS.NVP_PRIORITY_MODE]: ValueOf; diff --git a/src/components/FullstoryNative.tsx b/src/components/FullstoryNative.tsx new file mode 100644 index 000000000000..63e6217a464e --- /dev/null +++ b/src/components/FullstoryNative.tsx @@ -0,0 +1,3 @@ +import FS from '@fullstory/react-native'; + +export default FS; diff --git a/src/components/VideoPlayer/BaseVideoPlayer.tsx b/src/components/VideoPlayer/BaseVideoPlayer.tsx index 980ef8c8ed8d..bc7c32729c5a 100644 --- a/src/components/VideoPlayer/BaseVideoPlayer.tsx +++ b/src/components/VideoPlayer/BaseVideoPlayer.tsx @@ -327,6 +327,7 @@ function BaseVideoPlayer({ ) : ( { if (!el) { diff --git a/src/libs/Fullstory/index.native.ts b/src/libs/Fullstory/index.native.ts new file mode 100644 index 000000000000..67d7c7f2fe90 --- /dev/null +++ b/src/libs/Fullstory/index.native.ts @@ -0,0 +1,52 @@ +import FullStory, {FSPage} from '@fullstory/react-native'; +import type {OnyxEntry} from 'react-native-onyx'; +import type {UserMetadata} from '@src/types/onyx'; + +/** + * Fullstory React-Native lib adapter + * Proxy function calls to React-Native lib + * */ +const FS = { + /** + * Sets the identity as anonymous using the FullStory library. + */ + anonymize: () => FullStory.anonymize(), + + /** + * Sets the identity consent status using the FullStory library. + */ + consent: (c: boolean) => FullStory.consent(c), + + /** + * Initializes the FullStory metadata with the provided metadata information. + */ + consentAndIdentify: (value: OnyxEntry) => { + try { + // We only use FullStory in production environment + FullStory.consent(true); + FS.fsIdentify(value); + } catch (e) { + // error handler + } + }, + + /** + * Sets the FullStory user identity based on the provided metadata information. + * If the metadata is null or the email is 'undefined', the user identity is anonymized. + * If the metadata contains an accountID, the user identity is defined with it. + */ + fsIdentify: (metadata: UserMetadata | null) => { + if (!metadata?.accountID) { + // anonymize FullStory user identity metadata + FullStory.anonymize(); + } else { + // define FullStory user identity + FullStory.identify(String(metadata.accountID), { + properties: metadata, + }); + } + }, +}; + +export default FS; +export {FSPage}; diff --git a/src/libs/Fullstory/index.ts b/src/libs/Fullstory/index.ts new file mode 100644 index 000000000000..5b60f20f9ddf --- /dev/null +++ b/src/libs/Fullstory/index.ts @@ -0,0 +1,94 @@ +import {FullStory, init, isInitialized} from '@fullstory/browser'; +import type {OnyxEntry} from 'react-native-onyx'; +import CONST from '@src/CONST'; +import * as Environment from '@src/libs/Environment/Environment'; +import type {UserMetadata} from '@src/types/onyx'; +import type NavigationProperties from './types'; + +// Placeholder Browser API does not support Manual Page definition +class FSPage { + private pageName; + + private properties; + + constructor(name: string, properties: NavigationProperties) { + this.pageName = name; + this.properties = properties; + } + + start() {} +} + +/** + * Web does not use Fullstory React-Native lib + * Proxy function calls to Browser Snippet instance + * */ +const FS = { + /** + * Executes a function when the FullStory library is ready, either by initialization or by observing the start event. + */ + onReady: () => + new Promise((resolve) => { + Environment.getEnvironment().then((envName: string) => { + if (CONST.ENVIRONMENT.PRODUCTION !== envName) { + return; + } + // Initialised via HEAD snippet + if (!isInitialized()) { + init({orgId: ''}, resolve); + } else { + FullStory('observe', {type: 'start', callback: resolve}); + } + }); + }), + + /** + * Sets the identity as anonymous using the FullStory library. + */ + anonymize: () => FullStory('setIdentity', {anonymous: true}), + + /** + * Sets the identity consent status using the FullStory library. + */ + consent: (c: boolean) => FullStory('setIdentity', {consent: c}), + + /** + * Initializes the FullStory metadata with the provided metadata information. + */ + consentAndIdentify: (value: OnyxEntry) => { + try { + Environment.getEnvironment().then((envName: string) => { + if (CONST.ENVIRONMENT.PRODUCTION !== envName) { + return; + } + FS.onReady().then(() => { + FS.consent(true); + FS.fsIdentify(value); + }); + }); + } catch (e) { + // error handler + } + }, + + /** + * Sets the FullStory user identity based on the provided metadata information. + * If the metadata does not contain an email, the user identity is anonymized. + * If the metadata contains an accountID, the user identity is defined with it. + */ + fsIdentify: (metadata: UserMetadata | null) => { + if (!metadata?.accountID) { + // anonymize FullStory user identity metadata + FS.anonymize(); + } else { + // define FullStory user identity + FullStory('setIdentity', { + uid: String(metadata.accountID), + properties: metadata, + }); + } + }, +}; + +export default FS; +export {FSPage}; diff --git a/src/libs/Fullstory/types.ts b/src/libs/Fullstory/types.ts new file mode 100644 index 000000000000..24878fd0fbd3 --- /dev/null +++ b/src/libs/Fullstory/types.ts @@ -0,0 +1,5 @@ +type NavigationProperties = { + path: string; +}; + +export default NavigationProperties; diff --git a/src/libs/Navigation/NavigationRoot.tsx b/src/libs/Navigation/NavigationRoot.tsx index 506eae2bdfd2..06a3dce8d59a 100644 --- a/src/libs/Navigation/NavigationRoot.tsx +++ b/src/libs/Navigation/NavigationRoot.tsx @@ -6,6 +6,7 @@ import useActiveWorkspace from '@hooks/useActiveWorkspace'; import useCurrentReportID from '@hooks/useCurrentReportID'; import useTheme from '@hooks/useTheme'; import useWindowDimensions from '@hooks/useWindowDimensions'; +import {FSPage} from '@libs/Fullstory'; import Log from '@libs/Log'; import {getPathFromURL} from '@libs/Url'; import {updateLastVisitedPath} from '@userActions/App'; @@ -57,6 +58,12 @@ function parseAndLogRoute(state: NavigationState) { } Navigation.setIsNavigationReady(); + + // Fullstory Page navigation tracking + const focusedRouteName = focusedRoute?.name; + if (focusedRouteName) { + new FSPage(focusedRouteName, {path: currentPath}).start(); + } } function NavigationRoot({authenticated, lastVisitedPath, initialUrl, onReady}: NavigationRootProps) { diff --git a/src/libs/actions/Session/index.ts b/src/libs/actions/Session/index.ts index 9fc1485cd6e5..303517558206 100644 --- a/src/libs/actions/Session/index.ts +++ b/src/libs/actions/Session/index.ts @@ -23,6 +23,7 @@ import type SignInUserParams from '@libs/API/parameters/SignInUserParams'; import {READ_COMMANDS, SIDE_EFFECT_REQUEST_COMMANDS, WRITE_COMMANDS} from '@libs/API/types'; import * as Authentication from '@libs/Authentication'; import * as ErrorUtils from '@libs/ErrorUtils'; +import Fullstory from '@libs/Fullstory'; import HttpUtils from '@libs/HttpUtils'; import Log from '@libs/Log'; import Navigation from '@libs/Navigation/Navigation'; @@ -64,6 +65,11 @@ Onyx.connect({ }, }); +Onyx.connect({ + key: ONYXKEYS.USER_METADATA, + callback: Fullstory.consentAndIdentify, +}); + let stashedSession: Session = {}; Onyx.connect({ key: ONYXKEYS.STASHED_SESSION, diff --git a/src/types/onyx/UserMetadata.ts b/src/types/onyx/UserMetadata.ts new file mode 100644 index 000000000000..fc6490264087 --- /dev/null +++ b/src/types/onyx/UserMetadata.ts @@ -0,0 +1,8 @@ +type UserMetadata = { + planType?: string; + role?: string; + freeTrial?: boolean; + accountID?: number; +}; + +export default UserMetadata; diff --git a/src/types/onyx/index.ts b/src/types/onyx/index.ts index eb3ef2eefc8d..d0fcae44ef31 100644 --- a/src/types/onyx/index.ts +++ b/src/types/onyx/index.ts @@ -74,6 +74,7 @@ import type {TransactionViolation, ViolationName} from './TransactionViolation'; import type TransactionViolations from './TransactionViolation'; import type User from './User'; import type UserLocation from './UserLocation'; +import type UserMetadata from './UserMetadata'; import type UserWallet from './UserWallet'; import type WalletAdditionalDetails from './WalletAdditionalDetails'; import type {WalletAdditionalQuestionDetails} from './WalletAdditionalDetails'; @@ -158,6 +159,7 @@ export type { TransactionViolations, User, UserLocation, + UserMetadata, UserWallet, ViolationName, WalletAdditionalDetails, diff --git a/web/gtm.js b/web/gtm.js deleted file mode 100644 index 6194d4ca2453..000000000000 --- a/web/gtm.js +++ /dev/null @@ -1,10 +0,0 @@ -(function (w, d, s, l, i) { - w[l] = w[l] || []; - w[l].push({'gtm.start': new Date().getTime(), event: 'gtm.js'}); - const f = d.getElementsByTagName(s)[0]; - const j = d.createElement(s); - const dl = l !== 'dataLayer' ? '&l=' + l : ''; - j.async = true; - j.src = 'https://www.googletagmanager.com/gtm.js?id=' + i + dl; - f.parentNode.insertBefore(j, f); -})(window, document, 'script', 'dataLayer', 'GTM-N4M3FLJZ'); diff --git a/web/index.html b/web/index.html index 115803573bbd..be6396190d25 100644 --- a/web/index.html +++ b/web/index.html @@ -122,9 +122,9 @@ <% if (htmlWebpackPlugin.options.isProduction) { %> - - - + + + <% } %> <% } %> diff --git a/web/thirdPartyScripts.js b/web/thirdPartyScripts.js new file mode 100644 index 000000000000..15e77dbd490e --- /dev/null +++ b/web/thirdPartyScripts.js @@ -0,0 +1,155 @@ +/* eslint-disable */ +// Google Tag Manager +(function (w, d, s, l, i) { + w[l] = w[l] || []; + w[l].push({'gtm.start': new Date().getTime(), event: 'gtm.js'}); + const f = d.getElementsByTagName(s)[0]; + const j = d.createElement(s); + const dl = l !== 'dataLayer' ? '&l=' + l : ''; + j.async = true; + j.src = 'https://www.googletagmanager.com/gtm.js?id=' + i + dl; + f.parentNode.insertBefore(j, f); +})(window, document, 'script', 'dataLayer', 'GTM-N4M3FLJZ'); + +// FullStory +window['_fs_host'] = 'fullstory.com'; +window['_fs_script'] = 'edge.fullstory.com/s/fs.js'; +window['_fs_org'] = 'o-1WN56P-na1'; +window['_fs_namespace'] = 'FS'; +!(function (m, n, e, t, l, o, g, y) { + var s, + f, + a = (function (h) { + return !(h in m) || (m.console && m.console.log && m.console.log('FullStory namespace conflict. Please set window["_fs_namespace"].'), !1); + })(e); + function p(b) { + var h, + d = []; + function j() { + h && + (d.forEach(function (b) { + var d; + try { + d = b[h[0]] && b[h[0]](h[1]); + } catch (h) { + return void (b[3] && b[3](h)); + } + d && d.then ? d.then(b[2], b[3]) : b[2] && b[2](d); + }), + (d.length = 0)); + } + function r(b) { + return function (d) { + h || ((h = [b, d]), j()); + }; + } + return ( + b(r(0), r(1)), + { + then: function (b, h) { + return p(function (r, i) { + d.push([b, h, r, i]), j(); + }); + }, + } + ); + } + a && + ((g = m[e] = + (function () { + var b = function (b, d, j, r) { + function i(i, c) { + h(b, d, j, i, c, r); + } + r = r || 2; + var c, + u = /Async$/; + return u.test(b) ? ((b = b.replace(u, '')), 'function' == typeof Promise ? new Promise(i) : p(i)) : h(b, d, j, c, c, r); + }; + function h(h, d, j, r, i, c) { + return b._api ? b._api(h, d, j, r, i, c) : (b.q && b.q.push([h, d, j, r, i, c]), null); + } + return (b.q = []), b; + })()), + (y = function (b) { + function h(h) { + 'function' == typeof h[4] && h[4](new Error(b)); + } + var d = g.q; + if (d) { + for (var j = 0; j < d.length; j++) h(d[j]); + (d.length = 0), (d.push = h); + } + }), + (function () { + ((o = n.createElement(t)).async = !0), + (o.crossOrigin = 'anonymous'), + (o.src = 'https://' + l), + (o.onerror = function () { + y('Error loading ' + l); + }); + var b = n.getElementsByTagName(t)[0]; + b && b.parentNode ? b.parentNode.insertBefore(o, b) : n.head.appendChild(o); + })(), + (function () { + function b() {} + function h(b, h, d) { + g(b, h, d, 1); + } + function d(b, d, j) { + h('setProperties', {type: b, properties: d}, j); + } + function j(b, h) { + d('user', b, h); + } + function r(b, h, d) { + j( + { + uid: b, + }, + d, + ), + h && j(h, d); + } + (g.identify = r), + (g.setUserVars = j), + (g.identifyAccount = b), + (g.clearUserCookie = b), + (g.setVars = d), + (g.event = function (b, d, j) { + h( + 'trackEvent', + { + name: b, + properties: d, + }, + j, + ); + }), + (g.anonymize = function () { + r(!1); + }), + (g.shutdown = function () { + h('shutdown'); + }), + (g.restart = function () { + h('restart'); + }), + (g.log = function (b, d) { + h('log', {level: b, msg: d}); + }), + (g.consent = function (b) { + h('setIdentity', {consent: !arguments.length || b}); + }); + })(), + (s = 'fetch'), + (f = 'XMLHttpRequest'), + (g._w = {}), + (g._w[f] = m[f]), + (g._w[s] = m[s]), + m[s] && + (m[s] = function () { + return g._w[s].apply(this, arguments); + }), + (g._v = '2.0.0')); +})(window, document, window._fs_namespace, 'script', window._fs_script);