feat(amplitude): add mobile implementation

This commit is contained in:
Hristo Terezov 2019-01-24 15:37:55 +00:00 committed by Saúl Ibarra Corretgé
parent 19b4b92150
commit 59fc3642a6
17 changed files with 326 additions and 39 deletions

View File

@ -45,8 +45,6 @@ deploy-appbundle:
$(OUTPUT_DIR)/analytics-ga.js \
$(BUILD_DIR)/analytics-ga.min.js \
$(BUILD_DIR)/analytics-ga.min.map \
$(BUILD_DIR)/analytics-amplitude.min.js \
$(BUILD_DIR)/analytics-amplitude.min.map \
$(DEPLOY_DIR)
deploy-lib-jitsi-meet:

View File

@ -25,6 +25,7 @@ dependencies {
implementation "com.android.support:appcompat-v7:${rootProject.ext.supportLibVersion}"
implementation 'org.webkit:android-jsc:+'
implementation 'com.amplitude:android-sdk:2.14.1'
implementation 'com.dropbox.core:dropbox-core-sdk:3.0.8'
api 'com.facebook.react:react-native:+'

View File

@ -53,6 +53,7 @@ class ReactInstanceManagerHolder {
new PictureInPictureModule(reactContext),
new ProximityModule(reactContext),
new WiFiStatsModule(reactContext),
new org.jitsi.meet.sdk.analytics.AmplitudeModule(reactContext),
new org.jitsi.meet.sdk.dropbox.Dropbox(reactContext),
new org.jitsi.meet.sdk.net.NAT64AddrInfoModule(reactContext)));

View File

@ -0,0 +1,87 @@
/*
* Copyright @ 2019-present 8x8, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jitsi.meet.sdk.analytics;
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.amplitude.api.Amplitude;
import org.json.JSONException;
import org.json.JSONObject;
/**
* Implements the react-native module for the Amplitude integration.
*/
public class AmplitudeModule
extends ReactContextBaseJavaModule {
public AmplitudeModule(ReactApplicationContext reactContext) {
super(reactContext);
}
/**
* Initializes the Amplitude SDK.
*
* @param instanceName The name of the Amplitude instance. Should
* be used only for multi-project logging.
* @param apiKey The API_KEY of the Amplitude project.
*/
@ReactMethod
public void init(String instanceName, String apiKey) {
Amplitude.getInstance(instanceName).initialize(getCurrentActivity(), apiKey);
}
/**
* Sets the user properties for an Amplitude instance.
*
* @param instanceName The name of the Amplitude instance.
* @param userProps JSON string with user properties to be set.
*/
@ReactMethod
public void setUserProperties(String instanceName, ReadableMap userProps) {
if (userProps != null) {
Amplitude.getInstance(instanceName).setUserProperties(
new JSONObject(userProps.toHashMap()));
}
}
/**
* Log an analytics event.
*
* @param instanceName The name of the Amplitude instance.
* @param eventType The event type.
* @param eventPropsString JSON string with the event properties.
*/
@ReactMethod
public void logEvent(String instanceName, String eventType, String eventPropsString) {
JSONObject eventProps = null;
try {
eventProps = new JSONObject(eventPropsString);
Amplitude.getInstance(instanceName).logEvent(eventType, eventProps);
} catch (JSONException e) {
e.printStackTrace();
}
}
@Override
public String getName() {
return "Amplitude";
}
}

View File

@ -35,6 +35,7 @@ target 'JitsiMeet' do
pod 'Folly',
:podspec => '../node_modules/react-native/third-party-podspecs/Folly.podspec'
pod 'Amplitude-iOS', '~> 4.0.4'
pod 'ObjectiveDropboxOfficial'
pod 'react-native-background-timer',

View File

@ -1,4 +1,5 @@
PODS:
- Amplitude-iOS (4.0.4)
- boost-for-react-native (1.63.0)
- BVLinearGradient (2.5.3):
- React
@ -154,6 +155,7 @@ PODS:
- yoga (0.57.8.React)
DEPENDENCIES:
- Amplitude-iOS (~> 4.0.4)
- BVLinearGradient (from `../node_modules/react-native-linear-gradient`)
- Crashlytics
- DoubleConversion (from `../node_modules/react-native/third-party-podspecs/DoubleConversion.podspec`)
@ -185,6 +187,7 @@ DEPENDENCIES:
SPEC REPOS:
https://github.com/cocoapods/specs.git:
- Amplitude-iOS
- boost-for-react-native
- Crashlytics
- Fabric
@ -235,6 +238,7 @@ EXTERNAL SOURCES:
:path: "../node_modules/react-native/ReactCommon/yoga"
SPEC CHECKSUMS:
Amplitude-iOS: 2ad4d7270c99186236c1272a3a9425463b1ae1a7
boost-for-react-native: 39c7adb57c4e60d6c5479dd8623128eb5b3f0f2c
BVLinearGradient: 0d985ec461359c82bc254f26d11008bdae50d17a
Crashlytics: 07fb167b1694128c1c9a5a5cc319b0e9c3ca0933
@ -268,6 +272,6 @@ SPEC CHECKSUMS:
SDWebImage: c5594f1a19c48d526d321e548902b56b479cd508
yoga: 74cdf036c30820443b25ade59916236b1e95ee93
PODFILE CHECKSUM: 7d1909450626f31f9ea2de80122a66a50af2e1ea
PODFILE CHECKSUM: b300161e95d65c24b91368803afb8873f4b873cc
COCOAPODS: 1.6.0
COCOAPODS: 1.6.1

View File

@ -34,6 +34,7 @@
75635B0B20751D6D00F29C9F /* left.wav in Resources */ = {isa = PBXBuildFile; fileRef = 75635B0920751D6D00F29C9F /* left.wav */; };
87FE6F3321E52437004A5DC7 /* incomingMessage.wav in Resources */ = {isa = PBXBuildFile; fileRef = 87FE6F3221E52437004A5DC7 /* incomingMessage.wav */; };
A4414AE020B37F1A003546E6 /* rejected.wav in Resources */ = {isa = PBXBuildFile; fileRef = A4414ADF20B37F1A003546E6 /* rejected.wav */; };
A480429C21EE335600289B73 /* AmplitudeModule.m in Sources */ = {isa = PBXBuildFile; fileRef = A480429B21EE335600289B73 /* AmplitudeModule.m */; };
A4A934E9212F3ADB001E9388 /* Dropbox.m in Sources */ = {isa = PBXBuildFile; fileRef = A4A934E8212F3ADB001E9388 /* Dropbox.m */; };
C6245F5D2053091D0040BE68 /* image-resize@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = C6245F5B2053091D0040BE68 /* image-resize@2x.png */; };
C6245F5E2053091D0040BE68 /* image-resize@3x.png in Resources */ = {isa = PBXBuildFile; fileRef = C6245F5C2053091D0040BE68 /* image-resize@3x.png */; };
@ -80,6 +81,7 @@
98E09B5C73D9036B4ED252FC /* Pods-JitsiMeet.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-JitsiMeet.debug.xcconfig"; path = "../Pods/Target Support Files/Pods-JitsiMeet/Pods-JitsiMeet.debug.xcconfig"; sourceTree = "<group>"; };
9C77CA3CC919B081F1A52982 /* Pods-JitsiMeet.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-JitsiMeet.release.xcconfig"; path = "../Pods/Target Support Files/Pods-JitsiMeet/Pods-JitsiMeet.release.xcconfig"; sourceTree = "<group>"; };
A4414ADF20B37F1A003546E6 /* rejected.wav */ = {isa = PBXFileReference; lastKnownFileType = audio.wav; name = rejected.wav; path = ../../sounds/rejected.wav; sourceTree = "<group>"; };
A480429B21EE335600289B73 /* AmplitudeModule.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = AmplitudeModule.m; path = src/analytics/AmplitudeModule.m; sourceTree = SOURCE_ROOT; };
A4A934E8212F3ADB001E9388 /* Dropbox.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = Dropbox.m; sourceTree = "<group>"; };
A4A934EB21349A06001E9388 /* Dropbox.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = Dropbox.h; sourceTree = "<group>"; };
C6245F5B2053091D0040BE68 /* image-resize@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; name = "image-resize@2x.png"; path = "src/picture-in-picture/image-resize@2x.png"; sourceTree = "<group>"; };
@ -148,6 +150,7 @@
0BD906E71EC0C00300C8C18E /* src */ = {
isa = PBXGroup;
children = (
A480429821ECE2D800289B73 /* analytics */,
0BB9AD7C1F60356D001C08DB /* AppInfo.m */,
0BCA495C1EC4B6C600B793EE /* AudioMode.m */,
C69EFA02209A0EFD0027712B /* callkit */,
@ -183,6 +186,15 @@
name = Frameworks;
sourceTree = "<group>";
};
A480429821ECE2D800289B73 /* analytics */ = {
isa = PBXGroup;
children = (
A480429B21EE335600289B73 /* AmplitudeModule.m */,
);
name = analytics;
path = "New Group";
sourceTree = "<group>";
};
A4A934E7212F3AB8001E9388 /* dropbox */ = {
isa = PBXGroup;
children = (
@ -374,6 +386,10 @@
);
inputPaths = (
"${PODS_ROOT}/Target Support Files/Pods-JitsiMeet/Pods-JitsiMeet-resources.sh",
"${PODS_ROOT}/Amplitude-iOS/Amplitude/api.amplitude.com.der",
"${PODS_ROOT}/Amplitude-iOS/Amplitude/ComodoCaLimitedRsaCertificationAuthority.der",
"${PODS_ROOT}/Amplitude-iOS/Amplitude/ComodoRsaCA.der",
"${PODS_ROOT}/Amplitude-iOS/Amplitude/ComodoRsaDomainValidationCA.der",
"${PODS_ROOT}/GoogleSignIn/Resources/GoogleSignIn.bundle",
"${PODS_ROOT}/../../node_modules/react-native-vector-icons/Fonts/AntDesign.ttf",
"${PODS_ROOT}/../../node_modules/react-native-vector-icons/Fonts/Entypo.ttf",
@ -393,6 +409,10 @@
);
name = "[CP] Copy Pods Resources";
outputPaths = (
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/api.amplitude.com.der",
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/ComodoCaLimitedRsaCertificationAuthority.der",
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/ComodoRsaCA.der",
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/ComodoRsaDomainValidationCA.der",
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/GoogleSignIn.bundle",
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/AntDesign.ttf",
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/Entypo.ttf",
@ -433,6 +453,7 @@
0BCA495F1EC4B6C600B793EE /* AudioMode.m in Sources */,
0B44A0191F902126009D1D64 /* MPVolumeViewManager.m in Sources */,
0BCA49611EC4B6C600B793EE /* Proximity.m in Sources */,
A480429C21EE335600289B73 /* AmplitudeModule.m in Sources */,
C69EFA0C209A0F660027712B /* JMCallKitEmitter.swift in Sources */,
C6A34261204EF76800E062DD /* DragGestureController.swift in Sources */,
A4A934E9212F3ADB001E9388 /* Dropbox.m in Sources */,

View File

@ -0,0 +1,55 @@
/*
* Copyright @ 2018-present 8x8, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#import <React/RCTBridgeModule.h>
#import "Amplitude.h"
@interface AmplitudeModule : NSObject<RCTBridgeModule>
@end
@implementation AmplitudeModule
RCT_EXPORT_MODULE(Amplitude)
+ (BOOL)requiresMainQueueSetup {
return NO;
}
RCT_EXPORT_METHOD(init:(NSString*)instanceName API_KEY:(NSString*)apiKey) {
[[Amplitude instanceWithName:instanceName] initializeApiKey:apiKey];
}
RCT_EXPORT_METHOD(setUserProperties:(NSString*)instanceName userPropsString:(NSDictionary*)userProps) {
if (userProps != nil) {
[[Amplitude instanceWithName:instanceName] setUserProperties:userProps];
}
}
RCT_EXPORT_METHOD(logEvent:(NSString*)instanceName eventType:(NSString*)eventType eventPropsString:(NSString*)eventPropsString) {
NSError *error;
NSData *eventPropsData = [eventPropsString dataUsingEncoding:NSUTF8StringEncoding];
NSDictionary *eventProperties = [NSJSONSerialization JSONObjectWithData:eventPropsData
options:NSJSONReadingMutableContainers
error:&error];
if (eventProperties == nil) {
NSLog(@"[Amplitude handler] Error parsing event properties: %@", error);
} else {
[[Amplitude instanceWithName:instanceName] logEvent:eventType withEventProperties:eventProperties];
}
}
@end

View File

@ -6,6 +6,8 @@ import JitsiMeetJS, {
} from '../base/lib-jitsi-meet';
import { getJitsiMeetGlobalNS, loadScript } from '../base/util';
import { AmplitudeHandler } from './handlers';
const logger = require('jitsi-meet-logger').getLogger(__filename);
/**
@ -23,6 +25,16 @@ export function sendAnalytics(event: Object) {
}
}
/**
* Resets the analytics adapter to its initial state - removes handlers, cache,
* disabled state, etc.
*
* @returns {void}
*/
export function resetAnalytics() {
analytics.reset();
}
/**
* Loads the analytics scripts and inits JitsiMeetJS.analytics by setting
* permanent properties and setting the handlers from the loaded scripts.
@ -43,6 +55,9 @@ export function initAnalytics({ getState }: { getState: Function }) {
const state = getState();
const config = state['features/base/config'];
const { locationURL } = state['features/base/connection'];
const host = locationURL ? locationURL.host : '';
const {
analytics: analyticsConfig = {},
deploymentInfo
@ -58,6 +73,7 @@ export function initAnalytics({ getState }: { getState: Function }) {
envType: (deploymentInfo && deploymentInfo.envType) || 'dev',
googleAnalyticsTrackingId,
group,
host,
product: deploymentInfo && deploymentInfo.product,
subproduct: deploymentInfo && deploymentInfo.environment,
user: user && user.id,
@ -109,7 +125,7 @@ export function initAnalytics({ getState }: { getState: Function }) {
* successfully loaded and rejects if there are no handlers loaded or the
* analytics is disabled.
*/
function _loadHandlers(scriptURLs, handlerConstructorOptions) {
function _loadHandlers(scriptURLs = [], handlerConstructorOptions) {
const promises = [];
for (const url of scriptURLs) {
@ -139,12 +155,12 @@ function _loadHandlers(scriptURLs, handlerConstructorOptions) {
// check the old location to provide legacy support
const analyticsHandlers = [
...getJitsiMeetGlobalNS().analyticsHandlers,
...window.analyticsHandlers
];
...window.analyticsHandlers,
if (analyticsHandlers.length === 0) {
throw new Error('No analytics handlers available');
}
// NOTE: when we add second handler it will be good to put all
// build-in handlers in an array and destruct it here.
AmplitudeHandler
];
const handlers = [];

View File

@ -1,15 +1,12 @@
import amplitude from 'amplitude-js';
import { getJitsiMeetGlobalNS } from '../../base/util';
import AbstractHandler from './AbstractHandler';
import { amplitude } from './amplitude';
const logger = require('jitsi-meet-logger').getLogger(__filename);
/**
* Analytics handler for Amplitude.
*/
class AmplitudeHandler extends AbstractHandler {
export default class AmplitudeHandler extends AbstractHandler {
/**
* Creates new instance of the Amplitude analytics handler.
*
@ -20,18 +17,22 @@ class AmplitudeHandler extends AbstractHandler {
constructor(options) {
super();
const { amplitudeAPPKey } = options;
const { amplitudeAPPKey, host } = options;
if (!amplitudeAPPKey) {
logger.warn(
'Failed to initialize Amplitude handler, no tracking ID');
'Failed to initialize Amplitude handler, no APP key');
return;
}
this._enabled = true;
amplitude.getInstance().init(amplitudeAPPKey);
this._amplitudeOptions = {
host
};
amplitude.getInstance(this._amplitudeOptions).init(amplitudeAPPKey);
}
/**
@ -42,7 +43,8 @@ class AmplitudeHandler extends AbstractHandler {
*/
setUserProperties(userProps) {
if (this._enabled) {
amplitude.getInstance().setUserProperties(userProps);
amplitude.getInstance(this._amplitudeOptions)
.setUserProperties(userProps);
}
}
@ -59,13 +61,8 @@ class AmplitudeHandler extends AbstractHandler {
return;
}
amplitude.getInstance().logEvent(
amplitude.getInstance(this._amplitudeOptions).logEvent(
this._extractName(event),
event);
}
}
const globalNS = getJitsiMeetGlobalNS();
globalNS.analyticsHandlers = globalNS.analyticsHandlers || [];
globalNS.analyticsHandlers.push(AmplitudeHandler);

View File

@ -0,0 +1,97 @@
import { NativeModules } from 'react-native';
const { Amplitude: AmplitudeNative } = NativeModules;
/**
* Wrapper for the Amplitude native module.
*/
class Amplitude {
/**
* Create new Amplitude instance.
*
* @param {string} instanceName - The name of the Amplitude instance. Should
* be used only for multi-project logging.
*/
constructor(instanceName) {
this._instanceName = instanceName;
}
/**
* Initializes the Amplitude SDK.
*
* @param {string} apiKey - The API_KEY of the Amplitude project.
* @returns {void}
*/
init(apiKey) {
AmplitudeNative.init(this._instanceName, apiKey);
}
/**
* Sets user properties for the current user.
*
* @param {Object} userProperties - The user properties to be set.
* @returns {void}
*/
setUserProperties(userProperties) {
AmplitudeNative.setUserProperties(this._instanceName, userProperties);
}
/**
* Log an event with eventType and eventProperties.
*
* @param {string} eventType - The type of the event.
* @param {Object} eventProperties - The properties of the event.
* @returns {void}
*/
logEvent(eventType, eventProperties) {
// The event properties are converted to JSON string because of known
// performance issue when passing objects trough the RN bridge too
// often (a few times a second).
AmplitudeNative.logEvent(
this._instanceName, eventType, JSON.stringify(eventProperties));
}
}
/**
* Cache of <tt>Amplitude</tt> instances by instanceName.
*/
const instances = {};
/**
* The default (with instanceName - undefined) <tt>Amplitude</tt> instance.
*/
let defaultInstance;
export default {
/**
* Returns a <tt>Amplitude</tt> instance.
*
* @param {Object} options - Optional parameters.
* @param {string} options.host - The host property from the current URL.
* @param {string|undefined} options.instanceName - The name of the
* amplitude instance. Should be used only for multi-project logging.
* @returns {Amplitude}
*/
getInstance(options = {}) {
let instance;
const { host = '', instanceName = '' } = options;
let internalInstanceName = host;
if (instanceName !== '') {
internalInstanceName += `-${instanceName}`;
}
if (internalInstanceName === '') {
instance = defaultInstance = defaultInstance || new Amplitude();
} else {
instance = instances[internalInstanceName]
= instances[internalInstanceName]
|| new Amplitude(internalInstanceName);
}
return instance;
}
};

View File

@ -0,0 +1,7 @@
import amplitude from 'amplitude-js';
export default {
getInstance(options = {}) {
return amplitude.getInstance(options.instanceName);
}
};

View File

@ -0,0 +1 @@
export { default as amplitude } from './Amplitude';

View File

@ -0,0 +1 @@
export { default as AmplitudeHandler } from './AmplitudeHandler';

View File

@ -1,7 +1,8 @@
import { SET_ROOM } from '../base/conference';
import { SET_CONFIG } from '../base/config';
import { MiddlewareRegistry } from '../base/redux';
import { initAnalytics } from './functions';
import { initAnalytics, resetAnalytics } from './functions';
/**
* Middleware which intercepts config actions to handle evaluating analytics
@ -12,6 +13,16 @@ import { initAnalytics } from './functions';
*/
MiddlewareRegistry.register(store => next => action => {
switch (action.type) {
case SET_CONFIG: {
if (navigator.product === 'ReactNative') {
// Reseting the analytics is currently not needed for web because
// the user will be redirected to another page and new instance of
// Analytics will be created and initialized.
resetAnalytics();
}
break;
}
case SET_ROOM: {
const result = next(action);

View File

@ -43,16 +43,7 @@ export function createLocalTrack(type: string, deviceId: string) {
* otherwise.
*/
export function isAnalyticsEnabled(stateful: Function | Object) {
const {
analytics = {},
disableThirdPartyRequests
} = toState(stateful)['features/base/config'];
const { scriptURLs } = analytics;
return (
!disableThirdPartyRequests
&& Array.isArray(scriptURLs)
&& Boolean(scriptURLs.length));
return !toState(stateful)['features/base/config'].disableThirdPartyRequests;
}
/**

View File

@ -135,9 +135,7 @@ module.exports = [
'./react/features/local-recording/'
+ 'recording/flac/flacEncodeWorker.js',
'analytics-ga':
'./react/features/analytics/handlers/GoogleAnalyticsHandler.js',
'analytics-amplitude':
'./react/features/analytics/handlers/AmplitudeHandler.js'
'./react/features/analytics/handlers/GoogleAnalyticsHandler.js'
}
}),