analytics-plugin-web3analytics
Version:
Web3 Analytics plugin for the Analytics project
281 lines (249 loc) • 12.7 kB
JavaScript
import { ComposeClient } from '@composedb/client';
import { definition } from './definition.js';
import { DID } from 'dids';
import { Secp256k1Provider } from 'key-did-provider-secp256k1';
import KeyResolver from 'key-did-resolver';
import { JsonRpcProvider, Contract, isAddress } from 'essential-eth'; // 2x as fast as ethers
import { JsonRpcProvider as EthersJsonRpcProvider, Web3Provider } from '@ethersproject/providers';
import { Contract as EthersContract } from '@ethersproject/contracts';
import { Wallet } from '@ethersproject/wallet';
import { RelayProvider } from "@opengsn/provider";
import { WrapBridge } from "@opengsn/provider/dist/WrapContract";
import { Eip1193Bridge } from "@ethersproject/experimental";
import { toString as u8aToString } from 'uint8arrays';
import { publicIpv4 } from 'public-ip';
import axios from 'axios';
import Web3AnalyticsABI from "./Web3AnalyticsABI.json";
import flatten from 'flat';
import log from 'loglevel';
log.setDefaultLevel("error");
const WEB3ANALYTICS_ADDRESS = '0xd00CCD251869086eCD8B54f328Df1d623369b8F6';
const WEB3ANALYTICS_PAYMASTER_ADDRESS = '0x1A387Db642a76bEE516f1e16F542ed64ee81772c';
const CERAMIC_ADDRESS = 'https://ceramic.web3analytics.network';
const GEO_API = 'https://web3analytics.network/api/ip/'; // Set up Ceramic ComposeDB
const compose = new ComposeClient({
ceramic: CERAMIC_ADDRESS,
definition
});
/**
* web3Analytics v3 plugin
* @param {object} userConfig - Plugin settings
* @param {string} pluginConfig.appId - The app ID (an ETH address) you received from web3 analytics (required)
* @param {string} pluginConfig.jsonRpcUrl - Your JSON RPC url (required)
* @param {string} pluginConfig.logLevel - Log level may be debug, info, warn, error (default). Param is optional
* @example
*
* web3Analytics({
* appId: 'YOUR_APP_ID',
* jsonRpcUrl: 'YOUR_JSONRPC_URL'
* })
*/
export default function web3Analytics(userConfig) {
const appId = userConfig.appId;
const jsonRpcUrl = userConfig.jsonRpcUrl;
const logLevel = userConfig.logLevel;
setLoglevel();
let authenticatedDID;
let geoInfo = {};
let q_ = Promise.resolve();
function setLoglevel() {
if (logLevel && logLevel.toString().toUpperCase() in log.levels) log.setLevel(logLevel, false);
} // `seed` must be a 32-byte long Uint8Array
async function authenticateCeramic(seed) {
const provider = new Secp256k1Provider(seed);
const did = new DID({
provider,
resolver: KeyResolver.getResolver()
});
log.debug(did); // Authenticate the DID with the provider
await did.authenticate();
log.debug(did); // The ComposeDB client can create and update streams using the authenticated DID
compose.setDID(did);
return did;
}
async function checkAppRegistration() {
if (!isAddress(appId)) return false;
const provider = new JsonRpcProvider(jsonRpcUrl);
const contract = await new Contract(WEB3ANALYTICS_ADDRESS, Web3AnalyticsABI, provider);
return contract.isAppRegistered(appId);
}
async function checkUserRegistration(signer) {
const provider = new JsonRpcProvider(jsonRpcUrl);
const contract = await new Contract(WEB3ANALYTICS_ADDRESS, Web3AnalyticsABI, provider);
return contract.isUserRegistered(appId, signer.address);
}
async function getGeoInfo() {
try {
const ip = await publicIpv4();
log.debug("IP: ", ip);
const res = await axios.get(`${GEO_API}${ip}`);
log.debug("Geo: ", res.data);
geoInfo = res.data;
} catch (error) {
console.error(error);
}
}
async function registerUser(privateKey, did) {
log.debug(did); // OpenGSN config
const signer = new Wallet(privateKey);
const gsnProvider = RelayProvider.newProvider({
provider: new WrapBridge(new Eip1193Bridge(signer, new EthersJsonRpcProvider(jsonRpcUrl))),
config: {
paymasterAddress: WEB3ANALYTICS_PAYMASTER_ADDRESS,
loggerConfiguration: {
logLevel: "debug"
}
}
});
await gsnProvider.init();
gsnProvider.addAccount(signer.privateKey);
const provider = new Web3Provider(gsnProvider);
const contract = await new EthersContract(WEB3ANALYTICS_ADDRESS, Web3AnalyticsABI, provider.getSigner(signer.address, signer.privateKey));
log.info(`Registering user Address: ${signer.address} did: ${did}`);
const transaction = await contract.addUser(did, appId);
setLoglevel(); // Needed b/c winston (opengsn) messes w/ console.log & loglevel loses it
log.debug(transaction);
const receipt = await provider.waitForTransaction(transaction.hash);
log.debug(receipt);
}
function queue(fn) {
q_ = q_.then(fn);
return q_;
}
async function sendEvent(payload, authenticatedDID) {
// Flatten payload
payload.geo = geoInfo;
let flattened = payload;
try {
flattened = flatten(payload, {
delimiter: '_'
});
} catch (err) {
log.error(err);
}
let cdbVariables = {
app_id: appId,
did: authenticatedDID.id,
created_at: new Date().toISOString(),
updated_at: Date.now(),
raw_payload: JSON.stringify(payload)
};
if (flattened.anonymousId) cdbVariables.anonymousId = flattened.anonymousId;
if (flattened.event) cdbVariables.event = flattened.event;
if (flattened.meta_ts) cdbVariables.meta_ts = payload.meta.ts.toString();
if (flattened.meta_rid) cdbVariables.meta_rid = flattened.meta_rid;
if (flattened.properties_url) cdbVariables.properties_url = flattened.properties_url;
if (flattened.properties_hash) cdbVariables.properties_hash = flattened.properties_hash;
if (flattened.properties_path) cdbVariables.properties_path = flattened.properties_path;
if (flattened.properties_title) cdbVariables.properties_title = flattened.properties_title;
if (flattened.properties_referrer) cdbVariables.properties_referrer = flattened.properties_referrer;
if (flattened.properties_search) cdbVariables.properties_search = flattened.properties_search;
if (flattened.properties_width) cdbVariables.properties_width = flattened.properties_width;
if (flattened.properties_height) cdbVariables.properties_height = flattened.properties_height;
if (flattened.traits_email) cdbVariables.traits_email = flattened.traits_email;
if (flattened.type) cdbVariables.type = flattened.type;
if (flattened.userId) cdbVariables.userId = flattened.userId;
if (flattened.geo_autonomousSystemNumber) cdbVariables.geo_autonomousSystemNumber = flattened.geo_autonomousSystemNumber;
if (flattened.geo_autonomousSystemOrganization) cdbVariables.geo_autonomousSystemOrganization = flattened.geo_autonomousSystemOrganization;
if (flattened.geo_city_geonameId) cdbVariables.geo_city_geonameId = flattened.geo_city_geonameId;
if (flattened.geo_city_name) cdbVariables.geo_city_name = flattened.geo_city_name;
if (flattened.geo_continent_code) cdbVariables.geo_continent_code = flattened.geo_continent_code;
if (flattened.geo_continent_geonameId) cdbVariables.geo_continent_geonameId = flattened.geo_continent_geonameId;
if (flattened.geo_continent_name) cdbVariables.geo_continent_name = flattened.geo_continent_name;
if (flattened.geo_country_geonameId) cdbVariables.geo_country_geonameId = flattened.geo_country_geonameId;
if (flattened.geo_country_isoCode) cdbVariables.geo_country_isoCode = flattened.geo_country_isoCode;
if (flattened.geo_country_name) cdbVariables.geo_country_name = flattened.geo_country_name;
if (flattened.geo_location_accuracyRadius) cdbVariables.geo_location_accuracyRadius = flattened.geo_location_accuracyRadius;
if (flattened.geo_location_latitude) cdbVariables.geo_location_latitude = flattened.geo_location_latitude;
if (flattened.geo_location_longitude) cdbVariables.geo_location_longitude = flattened.geo_location_longitude;
if (flattened.geo_location_metroCode) cdbVariables.geo_location_metroCode = flattened.geo_location_metroCode;
if (flattened.geo_location_timeZone) cdbVariables.geo_location_timeZone = flattened.geo_location_timeZone;
if (flattened.geo_postal) cdbVariables.geo_postal = flattened.geo_postal;
if (flattened.geo_registeredCountry_geonameId) cdbVariables.geo_registeredCountry_geonameId = flattened.geo_registeredCountry_geonameId;
if (flattened.geo_registeredCountry_isoCode) cdbVariables.geo_registeredCountry_isoCode = flattened.geo_registeredCountry_isoCode;
if (flattened.geo_registeredCountry_name) cdbVariables.geo_registeredCountry_name = flattened.geo_registeredCountry_name;
if (flattened.geo_subdivision_geonameId) cdbVariables.geo_subdivision_geonameId = flattened.geo_subdivision_geonameId;
if (flattened.geo_subdivision_isoCode) cdbVariables.geo_subdivision_isoCode = flattened.geo_subdivision_isoCode;
if (flattened.geo_subdivision_name) cdbVariables.geo_subdivision_name = flattened.geo_subdivision_name;
if (flattened.hasOwnProperty('geo_traits_isAnonymous')) cdbVariables.geo_traits_isAnonymous = flattened.geo_traits_isAnonymous;
if (flattened.hasOwnProperty('geo_traits_isAnonymousProxy')) cdbVariables.geo_traits_isAnonymousProxy = flattened.geo_traits_isAnonymousProxy;
if (flattened.hasOwnProperty('geo_traits_isAnonymousVpn')) cdbVariables.geo_traits_isAnonymousVpn = flattened.geo_traits_isAnonymousVpn;
if (flattened.hasOwnProperty('geo_traits_isHostingProvider')) cdbVariables.geo_traits_isHostingProvider = flattened.geo_traits_isHostingProvider;
if (flattened.hasOwnProperty('geo_traits_isLegitimateProxy')) cdbVariables.geo_traits_isLegitimateProxy = flattened.geo_traits_isLegitimateProxy;
if (flattened.hasOwnProperty('geo_traits_isPublicProxy')) cdbVariables.geo_traits_isPublicProxy = flattened.geo_traits_isPublicProxy;
if (flattened.hasOwnProperty('geo_traits_isResidentialProxy')) cdbVariables.geo_traits_isResidentialProxy = flattened.geo_traits_isResidentialProxy;
if (flattened.hasOwnProperty('geo_traits_isSatelliteProvider')) cdbVariables.geo_traits_isSatelliteProvider = flattened.geo_traits_isSatelliteProvider;
if (flattened.hasOwnProperty('geo_traits_isTorExitNode')) cdbVariables.geo_traits_isTorExitNode = flattened.geo_traits_isTorExitNode;
log.debug(cdbVariables); // Create Event using ComposeDB
const createResult = await compose.executeQuery(`
mutation CreateNewEvent($i: CreateEventInput!){
createEvent(input: $i){
document{
id
}
}
}
`, {
"i": {
"content": cdbVariables
}
});
log.debug(JSON.stringify(createResult, null, 2));
} // Return object for analytics to use
return {
name: 'web3analytics',
config: {},
initialize: async ({
config
}) => {
let seed;
const ceramicSeed = JSON.parse(localStorage.getItem('ceramicSeed'));
if (!ceramicSeed) {
// Create new seed
seed = crypto.getRandomValues(new Uint8Array(32));
localStorage.setItem('ceramicSeed', JSON.stringify(Array.from(seed)));
} else {
// Use existing seed
seed = new Uint8Array(JSON.parse(localStorage.getItem('ceramicSeed')));
}
const privateKey = "0x" + u8aToString(seed, 'base16'); // Authenticate Ceramic
authenticatedDID = await authenticateCeramic(seed);
localStorage.setItem('authenticatedDID', authenticatedDID.id); // Check app registration
const isAppRegistered = await checkAppRegistration();
if (!isAppRegistered) {
log.info(`${appId} is not a registered app. Tracking not enabled.`);
return;
}
log.info(`App is Registered: ${appId}`); // Check user registration
// TODO: allow tracking while user is being registered? put this in a web worker?
const signer = new Wallet(privateKey);
const isUserRegistered = await checkUserRegistration(signer);
if (!isUserRegistered) {
log.info(`User not registered. Attempting to register.`);
registerUser(privateKey, authenticatedDID.id);
} else {
log.info(`User is registered.`);
} // get geo info
await getGeoInfo(); // enable tracking
window.web3AnalyticsLoaded = true;
},
page: async ({
payload
}) => {
queue(sendEvent.bind(null, payload, authenticatedDID));
},
track: async ({
payload
}) => {
queue(sendEvent.bind(null, payload, authenticatedDID));
},
identify: async ({
payload
}) => {
queue(sendEvent.bind(null, payload, authenticatedDID));
},
loaded: () => {
return !!window.web3AnalyticsLoaded;
}
};
}