import { SourceIntegrationConstructor , SourceIntegrationInterface , ImpactEstimationResult , MetricValueParams , ExperimentMetricQueryResponse , PastExperimentResponse , MetricValueQueryResponse , ExperimentQueryResponses , } from "../types/Integration" ; import { GoogleAnalyticsParams } from "../../types/integrations/googleanalytics" ; import { decryptDataSourceParams } from "../services/datasource" ; import { analyticsreporting_v4 , google } from "googleapis" ; import { GOOGLE_OAUTH_CLIENT_ID , GOOGLE_OAUTH_CLIENT_SECRET , APP_ORIGIN , } from "../util/secrets" ; import { DataSourceProperties , DataSourceSettings , } from "../../types/datasource" ; import { ExperimentInterface , ExperimentPhase } from "../../types/experiment" ; import { MetricInterface } from "../../types/metric" ; export function getOauth2Client ( ) { return new google . auth . OAuth2 ( GOOGLE_OAUTH_CLIENT_ID , GOOGLE_OAUTH_CLIENT_SECRET , `${ APP_ORIGIN }/oauth/google` ) ; } function convertDate ( rawDate ) { if ( rawDate . match ( /^[0-9]{8}$/ ) ) { return ( rawDate . slice ( 0 , 4 ) + "-" + rawDate . slice ( 4 , 6 ) + "-" + rawDate . slice ( 6 , 8 ) + "T12:00:00Z" ) ; } return "" ; } const GoogleAnalytics = class implements SourceIntegrationInterface { params : GoogleAnalyticsParams ; datasource : string ; organization : string ; settings : DataSourceSettings ; constructor ( encryptedParams ) { this . params = decryptDataSourceParams < GoogleAnalyticsParams > ( encryptedParams ) ; this . settings = { } ; } getExperimentMetricQuery ( ) { throw new Error ( "Method not implemented." ) ; } runExperimentMetricQuery ( ) { throw new Error ( "Method not implemented." ) ; } getPastExperimentQuery ( ) { throw new Error ( "Method not implemented." ) ; } runPastExperimentQuery ( ) { throw new Error ( "Method not implemented." ) ; } getMetricValueQuery ( params ) { return JSON . stringify ( { viewId : this . params . viewId , dateRanges : [ { startDate : params . from . toISOString ( ) . substr ( 0 , 10 ) , endDate : params . to . toISOString ( ) . substr ( 0 , 10 ) , } , ] , metrics : [ { expression : params . metric . table , } , { expression : "ga:users" , } , ] , dimensions : [ { name : "ga:date" , } , ] , } , null , 2 ) ; } async runMetricValueQuery ( query ) { const { rows , metrics } = await this . runQuery ( query ) ; const dates = [ ] ; if ( rows ) { const metric = metrics [ 0 ] ; const isTotal = metric && metric !== "ga:bounceRate" && ! metric . match ( /^ga:avg/ ) ; const isBinomial = metric && ( metric === "ga:bounceRate" || metric . match ( /^ga:goal.*(Starts|Completions)$/ ) ) ; const isDuration = metric && [ "ga:avgPageLoadTime" , "avgSessionDuration" , "avgTimeOnPage" ] . includes ( metric ) ; rows . forEach ( ( row ) => { const date = convertDate ( row . dimensions ?. [ 0 ] || "" ) ; const value = parseFloat ( row . metrics ?. [ 0 ] ?. values ?. [ 0 ] || "" ) || 0 ; const users = parseInt ( row . metrics ?. [ 0 ] ?. values ?. [ 1 ] || "" ) || 0 ; let count ; let mean ; let stddev = 0 ; if ( metric === "ga:bounceRate" ) { count = Math . round ( ( users * value ) / 100 ) ; mean = 1 ; } else if ( isBinomial ) { count = value ; mean = 1 ; } else if ( isDuration ) { count = users ; mean = value ; stddev = mean ; } else if ( isTotal ) { count = users ; mean = value / users ; } else { count = users ; mean = value ; } dates . push ( { date , users , count , mean , stddev , } ) ; } ) ; } return dates ; } async runQuery ( query ) { const result = await google . analyticsreporting ( "v4" ) . reports . batchGet ( { auth : this . getAuth ( ) , requestBody : { reportRequests : [ JSON . parse ( query ) ] , } , } ) ; return { metrics : ( result ?. data ?. reports ?. [ 0 ] ?. columnHeader ?. metricHeader ?. metricHeaderEntries || [ ] ) . map ( ( m ) => m . name ) , rows : result ?. data ?. reports ?. [ 0 ] ?. data ?. rows , } ; } getSourceProperties ( ) { return { queryLanguage : "json" , } ; } async testConnection ( ) { this . getAuth ( ) ; return true ; } getSensitiveParamKeys ( ) { return [ "refreshToken" ] ; } getAuth ( ) { const client = getOauth2Client ( ) ; client . setCredentials ( { refresh_token : this . params . refreshToken , } ) ; return client ; } async getImpactEstimation ( ) { throw new Error ( "Not implemented for GA" ) ; } getExperimentResultsQuery ( experiment , phase , metrics ) { const metricExpressions = metrics . map ( ( m ) => ( { expression : m . table , } ) ) ; const query = { viewId : this . params . viewId , dateRanges : [ { startDate : phase . dateStarted . toISOString ( ) . substr ( 0 , 10 ) , endDate : ( phase . dateEnded || new Date ( ) ) . toISOString ( ) . substr ( 0 , 10 ) , } , ] , metrics : [ { expression : "ga:users" , } , ... metricExpressions , ] , dimensions : [ { name : `ga:dimension${ this . params . customDimension }` , } , ] , dimensionFilterClauses : [ { filters : [ { dimensionName : `ga:dimension${ this . params . customDimension }` , operator : "BEGINS_WITH" , expressions : [ experiment . trackingKey + this . getDelimiter ( ) ] , } , ] , } , ] , } ; return JSON . stringify ( query , null , 2 ) ; } private getDelimiter ( ) { return this . params . delimiter || ":" ; } async getExperimentResults ( experiment , phase , metrics ) { const query = this . getExperimentResultsQuery ( experiment , phase , metrics ) ; const result = await google . analyticsreporting ( "v4" ) . reports . batchGet ( { auth : this . getAuth ( ) , requestBody : { reportRequests : [ JSON . parse ( query ) ] , } , } ) ; const rows = result ?. data ?. reports ?. [ 0 ] ?. data ?. rows ; if ( ! rows ) { throw new Error ( "Failed to update" ) ; } return rows . map ( ( row ) => { const users = parseInt ( row . metrics ?. [ 0 ] ?. values ?. [ 0 ] || "" ) ; return { dimension : "" , variation : ( row . dimensions ?. [ 0 ] || "" ) . split ( this . getDelimiter ( ) , 2 ) [ 1 ] || "" , users : users || 0 , metrics : metrics . map ( ( metric , j ) => { let value = parseFloat ( row . metrics ?. [ 0 ] ?. values ?. [ j + 1 ] || "" ) || 0 ; if ( metric . table === "ga:bounceRate" ) { value = ( users * value ) / 100 ; } else if ( metric . table ?. match ( /^ga:avg/ ) ) { value = users * value ; } const mean = metric . type === "binomial" ? 1 : Math . round ( value ) / users ; const count = metric . type === "binomial" ? value : users ; const stddev = metric . type === "duration" ? mean : metric . type === "count" ? Math . sqrt ( mean ) : 0 ; return { metric : metric . id , users , count , mean , stddev , } ; } ) , } ; } ) ; } } ; export default GoogleAnalytics ;