Skip to content

Commit

Permalink
Usage analytics (#1502)
Browse files Browse the repository at this point in the history
  • Loading branch information
James William Dumay authored Oct 23, 2017
1 parent b998ba2 commit b2c2327
Show file tree
Hide file tree
Showing 18 changed files with 189 additions and 36 deletions.
15 changes: 15 additions & 0 deletions ANALYTICS_EVENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
Analytics Events
================

| Event Name | Description |
| ------------- | ------------- |
| dashboard_visited | Send when user visits the Dashboard |
| pageview | Send every time the page is visited or the view is changed |
| pipeline_activity_visited | Send every time the Pipeline activity tab is visited |
| pipeline_creation_visited | Sent when the user clicks the `New Pipeline` button |
| pipeline_branches_visited | Send every time the Pipeline branches tab is visited |
| pipeline_pull_requests_visited | Send every time the Pipeline pull requests tab is visited |
| pipeline_run_visited | Send every time the Pipeline run result is visited |
| pipeline_run_changes_visited | Send every time the Pipeline run changes is visited |
| pipeline_run_tests_visited | Send every time the Pipeline run tests is visited |

Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,13 @@
import com.fasterxml.jackson.databind.JsonMappingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.google.common.base.Charsets;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.Reader;
import java.io.StringReader;
import java.text.SimpleDateFormat;
Expand All @@ -28,6 +31,10 @@ public static <T> T toJava(String data, Class<T> type) {
return toJava(new StringReader(data), type);
}

public static <T> T toJava(InputStream data, Class<T> type) {
return toJava(new InputStreamReader(data, Charsets.UTF_8), type);
}

public static <T> T toJava(Reader data, Class<T> type) {
try {
return om.readValue(data, type);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,12 @@

import hudson.Extension;
import hudson.Plugin;
import hudson.model.UsageStatistics;
import hudson.security.AuthorizationStrategy;
import hudson.security.FullControlOnceLoggedInAuthorizationStrategy;
import hudson.security.SecurityRealm;
import hudson.util.VersionNumber;
import io.jenkins.blueocean.analytics.Analytics;
import io.jenkins.blueocean.auth.jwt.JwtTokenServiceEndpoint;
import io.jenkins.blueocean.commons.BlueOceanConfigProperties;
import io.jenkins.blueocean.commons.PageStatePreloader;
Expand Down Expand Up @@ -59,6 +61,7 @@ public String getStateJson() {
.key("version").value(getBlueOceanPluginVersion())
.key("jenkinsConfig")
.object()
.key("analytics").value(Analytics.isAnalyticsEnabled())
.key("version").value(version)
.key("security")
.object()
Expand All @@ -76,7 +79,7 @@ public String getStateJson() {

return writer.toString();
}

/**
* Adds all features from all BlueOceanConfig objects
* @param builder JSON builder to use
Expand Down
13 changes: 9 additions & 4 deletions blueocean-core-js/src/js/analytics/AnalyticsService.js
Original file line number Diff line number Diff line change
@@ -1,20 +1,25 @@
import { Fetch } from '../fetch';
import config from '../urlconfig';
import urlConfig from '../urlconfig';
import utils from '../utils';
import config from '../config';

export class AnalyticsService {
track(eventName, properties) {
const path = config.getJenkinsRootURL();
// Don't scare anyone by posting back stats tracking when usage stats are off
if (!config.getAnalyticsEnabled()) return false;

// Go ahead and record usage stats
const path = urlConfig.getJenkinsRootURL();
const url = utils.cleanSlashes(`${path}/blue/rest/analytics/track`);
const fetchOptions = {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: {
body: JSON.stringify({
name: eventName,
properties: { properties },
},
}),
};
return Fetch.fetch(url, { fetchOptions });
}
Expand Down
4 changes: 4 additions & 0 deletions blueocean-core-js/src/js/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,10 @@ export default {
return this.getJenkinsConfig().security || {};
},

getAnalyticsEnabled() {
return this.getJenkinsConfig().analytics || false;
},

isJWTEnabled() {
return !!this.getSecurityConfig().enableJWT;
},
Expand Down
8 changes: 6 additions & 2 deletions blueocean-core-js/src/js/fetch.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,10 @@ import { capabilityAugmenter } from './capability/index';

let refreshToken = null;

function isGetRequest(fetchOptions) {
return !fetchOptions || !fetchOptions.method || 'get'.localeCompare(fetchOptions.method) === 0;
}

export const FetchFunctions = {
/**
* Ensures the URL starts with jenkins path if not an absolute URL.
Expand Down Expand Up @@ -200,7 +204,7 @@ export const FetchFunctions = {

return future;
};
if (disableDedupe) {
if (disableDedupe || !isGetRequest(fetchOptions)) {
return request();
}

Expand Down Expand Up @@ -247,7 +251,7 @@ export const FetchFunctions = {
return future;
};

if (disableDedupe) {
if (disableDedupe || !isGetRequest(fetchOptions)) {
return request();
}

Expand Down
36 changes: 23 additions & 13 deletions blueocean-dashboard/src/main/js/PipelineRoutes.jsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { Route, Redirect, IndexRedirect } from 'react-router';
import React from 'react';
import { AppConfig } from '@jenkins-cd/blueocean-core-js';
import { analytics } from './analytics';

import Dashboard from './Dashboard';
import {
Expand Down Expand Up @@ -94,6 +95,10 @@ function isRemovePersistedBackgroundRoute(prevState, nextState) {
return isLeavingRunDetails(prevState, nextState);
}

function onTopLevelRouteEnter() {
analytics.trackPageView(); // Tracks the page view on load of window
}

/**
* Persists the application's DOM as a "background" when navigating to a route w/ a modal or dialog.
* Also removes the "background" when navigating away.
Expand All @@ -112,38 +117,43 @@ function persistBackgroundOnNavigationChange(prevState, nextState, replace, call
}
}

function onRouteChange(prevState, nextState, replace, callback, delay = 200) {
analytics.trackPageView(); // Tracks page view as the route changes
persistBackgroundOnNavigationChange(prevState, nextState, replace, callback, delay);
}

function onLeaveCheckBackground() {
persistBackgroundOnNavigationChange({ params: { runId: true } }, { params: {} }, null, null, 0);
}

const trends = AppConfig.isFeatureEnabled('trends');

export default (
<Route component={Dashboard} onChange={persistBackgroundOnNavigationChange}>
<Route path="organizations/:organization/pipelines" component={Pipelines} />
<Route path="organizations/:organization/create-pipeline" component={CreatePipeline} />
<Route component={Dashboard} onEnter={onTopLevelRouteEnter} onChange={onRouteChange}>
<Route path="organizations/:organization/pipelines" component={Pipelines} onEnter={analytics.trackDashboardVisited} />
<Route path="organizations/:organization/create-pipeline" component={CreatePipeline} onEnter={analytics.trackPipelineCreationVisited} />
<Redirect from="organizations/:organization(/*)" to="organizations/:organization/pipelines" />
<Route path="organizations/:organization" component={PipelinePage}>
<Route path=":pipeline/branches" component={MultiBranch} />
<Route path=":pipeline/activity" component={Activity} />
<Route path=":pipeline/pr" component={PullRequests} />
<Route path=":pipeline/branches" component={MultiBranch} onEnter={analytics.trackPipelineBranchesVisited} />
<Route path=":pipeline/activity" component={Activity} onEnter={analytics.trackPipelineActivityVisited} />
<Route path=":pipeline/pr" component={PullRequests} onEnter={analytics.trackPipelinePullRequestsVisited} />
{ trends && <Route path=":pipeline/trends" component={PipelineTrends} /> }

<Route path=":pipeline/detail/:branch/:runId" component={RunDetails} onLeave={onLeaveCheckBackground}>
<Route path=":pipeline/detail/:branch/:runId" component={RunDetails} onLeave={onLeaveCheckBackground} >
<IndexRedirect to="pipeline" />
<Route path="pipeline" component={RunDetailsPipeline}>
<Route path="pipeline" component={RunDetailsPipeline} onEnter={analytics.trackPipelineRunVisited}>
<Route path=":node" component={RunDetailsPipeline} />
</Route>
<Route path="changes" component={RunDetailsChanges} />
<Route path="tests" component={RunDetailsTests} />
<Route path="artifacts" component={RunDetailsArtifacts} />
<Route path="changes" component={RunDetailsChanges} onEnter={analytics.trackPipelineRunChangesVisited} />
<Route path="tests" component={RunDetailsTests} onEnter={analytics.trackPipelineRunTestsVisited} />
<Route path="artifacts" component={RunDetailsArtifacts} onEnter={analytics.trackPipelineRunArtifactsVisited} />
</Route>

<Redirect from=":pipeline(/*)" to=":pipeline/activity" />

</Route>
<Route path="/pipelines" component={Pipelines} />
<Route path="/create-pipeline" component={CreatePipeline} />
<Route path="/pipelines" component={Pipelines} onEnter={analytics.trackDashboardVisited} />
<Route path="/create-pipeline" component={CreatePipeline} onEnter={analytics.trackPipelineCreationVisited} />
<IndexRedirect to="pipelines" />
</Route>
);
50 changes: 50 additions & 0 deletions blueocean-dashboard/src/main/js/analytics.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { analyticsService } from '@jenkins-cd/blueocean-core-js';

/** codifies all events send to analytics service in this module */
class Analytics {
trackPageView() {
analyticsService.track('pageview', { mode: 'blueocean' });
}

trackPipelineActivityVisited() {
analyticsService.track('pipeline_activity_visited');
}

trackPipelineBranchesVisited() {
analyticsService.track('pipeline_branches_visited');
}

trackPipelinePullRequestsVisited() {
analyticsService.track('pipeline_pull_requests_visited');
}

trackDashboardVisited() {
analyticsService.track('dashboard_visited');
}

trackPipelineCreationVisited() {
analyticsService.track('pipeline_creation_visited');
}

trackPipelineRunVisited() {
analyticsService.track('pipeline_run_visited');
}

trackPipelineRunChangesVisited() {
analyticsService.track('pipeline_run_changes_visited');
}

trackPipelineRunTestsVisited() {
analyticsService.track('pipeline_run_tests_visited');
}

trackPipelineRunArtifactsVisited() {
analyticsService.track('pipeline_run_artifacts_visited');
}
}

const analytics = new Analytics();

export {
analytics,
};
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

import hudson.Extension;
import hudson.model.RootAction;
import hudson.model.UsageStatistics;
import io.jenkins.blueocean.analytics.Analytics;
import io.jenkins.blueocean.rest.factory.BlueOceanUrlObjectFactory;
import jenkins.model.Jenkins;

Expand Down Expand Up @@ -41,6 +43,10 @@ public String getUrlName() {
return BlueOceanUrlMapperImpl.getLandingPagePath();
}

public boolean isAnalyticsEnabled() {
return Analytics.isAnalyticsEnabled();
}

//lazy initialization in thread safe way
private void setBlueOceanUIProvider(){
BlueOceanUrlObjectFactory f = factory;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

import com.google.common.base.Preconditions;
import hudson.model.Action;
import hudson.model.UsageStatistics;
import io.jenkins.blueocean.analytics.Analytics;
import io.jenkins.blueocean.rest.model.BlueOceanUrlObject;

import javax.annotation.Nonnull;
Expand Down Expand Up @@ -35,4 +37,8 @@ public String getUrlName() {
BlueOceanUrlObject getBlueOceanUrlObject(){
return blueOceanUrlObject;
}

public boolean isAnalyticsEnabled() {
return Analytics.isAnalyticsEnabled();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -45,11 +45,15 @@ public void track(TrackRequest req) {
}
allProps.put("jenkins", server());
allProps.put("userId", identity());
String msg = Objects.toStringHelper(this).add("name", req.name).add("props", allProps).toString();
Objects.ToStringHelper eventHelper = Objects.toStringHelper(this).add("name", req.name).add("props", allProps);
try {
doTrack(req.name, allProps);
LOGGER.log(Level.FINE, msg);
if (LOGGER.isLoggable(Level.FINE)) {
String msg = eventHelper.toString();
LOGGER.log(Level.FINE, msg);
}
} catch (Throwable throwable) {
String msg = eventHelper.toString();
LOGGER.log(Level.WARNING, "Failed to send event: " + msg);
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
/**
* Tracks events using Keen.io
*/
@Extension
@Extension(ordinal = -1)
@Restricted(NoExternalUse.class)
public class KeenAnalyticsImpl extends AbstractAnalytics {

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,12 +35,12 @@ KeenProject project() {
public static class DefaultKeenConfiguration extends KeenConfiguration {
@Override
public String getProjectId() {
return "595421390ee24f5b59032f79";
return "5950934f3d5e150f5ab9d7be";
}

@Override
public String getWriteKey() {
return "3B9A2F5ECEEF0E4C22886A644443F1C994FE3D294C4A0520699671A452C4A017E0C28196ADF8DEBC31547BFC0BC32BE824E6C7300C3E3FB2C5D921B74F274FA95B63C17E0349AA970CDB35FDE519A364A26958B488E9A729BFD03A034BE42F56";
return "E6C1FA3407AF4DD3115DBC186E40E9183A90069B1D8BBA78DB3EA6B15EA6182C881E8C55B4D7A48F55D5610AD46F36E65093227A7490BF7A56307047903BCCB16D05B9456F18A66849048F100571FDC91888CAD94F2A271A8B9E5342D2B9404E";
}
}
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
<?jelly escape-by-default='true'?>
<j:jelly xmlns:j="jelly:core" xmlns:l="/lib/layout">
<j:if test="${action.analyticsEnabled}">
<script src="${rootURL}/plugin/blueocean-rest-impl/scripts/analytics.js"></script>
</j:if>
<l:task icon="${h.getIconFilePath(action)}" title="${action.displayName}"
href="${rootURL}/${action.urlName}"
permission="${app.READ}"
/>
</j:jelly>
</j:jelly>
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
<?jelly escape-by-default='true'?>
<j:jelly xmlns:j="jelly:core" xmlns:l="/lib/layout">
<j:if test="${action.analyticsEnabled}">
<script src="${rootURL}/plugin/blueocean-rest-impl/scripts/analytics.js"></script>
</j:if>
<l:task icon="${h.getIconFilePath(action)}" title="${action.displayName}"
href="${rootURL}/${action.urlName}"
permission="${app.READ}"
/>
</j:jelly>
</j:jelly>
10 changes: 10 additions & 0 deletions blueocean-rest-impl/src/main/webapp/scripts/analytics.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@

window.addEventListener('load', function() {
var eventData = { name: 'pageview', properties: { mode: 'classic' } };
new Ajax.Request(rootURL + '/blue/rest/analytics/track', {
method: 'POST',
contentType:'application/json',
postBody: JSON.stringify(eventData),
onFailure: function() { console.error('Could not send pageview event') },
});
});
Loading

0 comments on commit b2c2327

Please sign in to comment.