diff --git a/helpers/googleAnalytics.js b/helpers/googleAnalytics.js
new file mode 100644
index 00000000..00f00efe
--- /dev/null
+++ b/helpers/googleAnalytics.js
@@ -0,0 +1,62 @@
+const logger = require('winston');
+const ua = require('universal-analytics');
+const config = require('../config/speechConfig.js');
+const googleApiKey = config.analytics.googleId;
+
+function createServeEventParams (headers, ip, originalUrl) {
+  return {
+    eventCategory    : 'client requests',
+    eventAction      : 'serve request',
+    eventLabel       : originalUrl,
+    ipOverride       : ip,
+    userAgentOverride: headers['user-agent'],
+  };
+};
+
+function createPublishTimingEventParams (label, startTime, endTime, ip, headers) {
+  const durration = endTime - startTime;
+  return {
+    userTimingCategory    : 'lbrynet',
+    userTimingVariableName: 'publish',
+    userTimingTime        : durration,
+    userTimingLabel       : label,
+    uip                   : ip,
+    userAgentOverride     : headers['user-agent'],
+  };
+};
+
+function sendGoogleAnalyticsEvent (ip, params) {
+  const visitorId = ip.replace(/\./g, '-');
+  const visitor = ua(googleApiKey, visitorId, { strictCidFormat: false, https: true });
+  visitor.event(params, (err) => {
+    if (err) {
+      logger.error('Google Analytics Event Error >>', err);
+    }
+  });
+};
+
+function sendGoogleAnalyticsTiming (ip, params) {
+  const visitorId = ip.replace(/\./g, '-');
+  const visitor = ua(googleApiKey, visitorId, { strictCidFormat: false, https: true });
+  visitor.timing(params, (err) => {
+    if (err) {
+      logger.error('Google Analytics Event Error >>', err);
+    }
+    logger.debug(`Timing event successfully sent to google analytics`);
+  });
+};
+
+module.exports = {
+  sendGAServeEvent (headers, ip, originalUrl) {
+    const params = createServeEventParams(headers, ip, originalUrl);
+    sendGoogleAnalyticsEvent(ip, params);
+  },
+  sendGAAnonymousPublishTiming (headers, ip, originalUrl, startTime, endTime) {
+    const params = createPublishTimingEventParams('anonymous', startTime, endTime, ip, headers);
+    sendGoogleAnalyticsTiming(ip, params);
+  },
+  sendGAChannelPublishTiming (headers, ip, originalUrl, startTime, endTime) {
+    const params = createPublishTimingEventParams('anonymous', startTime, endTime, ip, headers);
+    sendGoogleAnalyticsTiming(ip, params);
+  },
+};
diff --git a/helpers/publishHelpers.js b/helpers/publishHelpers.js
index 5d315a49..a9cbf2bd 100644
--- a/helpers/publishHelpers.js
+++ b/helpers/publishHelpers.js
@@ -1,4 +1,3 @@
-const constants = require('../constants');
 const logger = require('winston');
 const fs = require('fs');
 const { site, wallet } = require('../config/speechConfig.js');
@@ -177,11 +176,4 @@ module.exports = {
       nsfw,
     };
   },
-  returnPublishTimingActionType (channelName) {
-    if (channelName) {
-      return constants.PUBLISH_IN_CHANNEL_CLAIM;
-    } else {
-      return constants.PUBLISH_ANONYMOUS_CLAIM;
-    }
-  },
 };
diff --git a/helpers/serveHelpers.js b/helpers/serveHelpers.js
index b3e1e61a..78447d6d 100644
--- a/helpers/serveHelpers.js
+++ b/helpers/serveHelpers.js
@@ -1,25 +1,109 @@
 const logger = require('winston');
+const { getClaimId, getLocalFileRecord } = require('../controllers/serveController.js');
+const { handleErrorResponse } = require('../helpers/errorHandlers.js');
+
+const SERVE = 'SERVE';
+const SHOW = 'SHOW';
+const NO_FILE = 'NO_FILE';
+const NO_CHANNEL = 'NO_CHANNEL';
+const NO_CLAIM = 'NO_CLAIM';
+
+function clientAcceptsHtml ({accept}) {
+  return accept && accept.match(/text\/html/);
+};
+
+function requestIsFromBrowser (headers) {
+  return headers['user-agent'] && headers['user-agent'].match(/Mozilla/);
+};
+
+function clientWantsAsset ({accept, range}) {
+  const imageIsWanted = accept && accept.match(/image\/.*/) && !accept.match(/text\/html/) && !accept.match(/text\/\*/);
+  const videoIsWanted = accept && range;
+  return imageIsWanted || videoIsWanted;
+};
+
+function isValidClaimId (claimId) {
+  return ((claimId.length === 40) && !/[^A-Za-z0-9]/g.test(claimId));
+};
+
+function isValidShortId (claimId) {
+  return claimId.length === 1;  // it should really evaluate the short url itself
+};
+
+function isValidShortIdOrClaimId (input) {
+  return (isValidClaimId(input) || isValidShortId(input));
+};
+
+function serveAssetToClient (claimId, name, res) {
+  return getLocalFileRecord(claimId, name)
+    .then(fileRecord => {
+      // check that a local record was found
+      if (fileRecord === NO_FILE) {
+        return res.status(307).redirect(`/api/claim/get/${name}/${claimId}`);
+      }
+      // serve the file
+      const {filePath, fileType} = fileRecord;
+      logger.verbose(`serving file: ${filePath}`);
+      const sendFileOptions = {
+        headers: {
+          'X-Content-Type-Options': 'nosniff',
+          'Content-Type'          : fileType || 'image/jpeg',
+        },
+      };
+      res.status(200).sendFile(filePath, sendFileOptions);
+    })
+    .catch(error => {
+      throw error;
+    });
+};
 
 module.exports = {
-  serveFile ({ filePath, fileType }, claimId, name, res) {
-    logger.verbose(`serving file: ${filePath}`);
-    // set response options
-    const headerContentType = fileType || 'image/jpeg';
-    const options = {
-      headers: {
-        'X-Content-Type-Options': 'nosniff',
-        'Content-Type'          : headerContentType,
-      },
-    };
-    // send the file
-    res.status(200).sendFile(filePath, options);
+  getClaimIdAndServeAsset (channelName, channelClaimId, claimName, claimId, originalUrl, ip, res) {
+    // get the claim Id and then serve the asset
+    getClaimId(channelName, channelClaimId, claimName, claimId)
+      .then(fullClaimId => {
+        if (fullClaimId === NO_CLAIM) {
+          return res.status(404).json({success: false, message: 'no claim id could be found'});
+        } else if (fullClaimId === NO_CHANNEL) {
+          return res.status(404).json({success: false, message: 'no channel id could be found'});
+        }
+        serveAssetToClient(fullClaimId, claimName, res);
+        // postToStats(responseType, originalUrl, ip, claimName, fullClaimId, 'success');
+      })
+      .catch(error => {
+        handleErrorResponse(originalUrl, ip, error, res);
+        // postToStats(responseType, originalUrl, ip, claimName, fullClaimId, 'fail');
+      });
   },
-  showFile (claimInfo, shortId, res) {
-    logger.verbose(`showing claim: ${claimInfo.name}#${claimInfo.claimId}`);
-    res.status(200).render('index');
+  determineResponseType (hasFileExtension, headers) {
+    let responseType;
+    if (hasFileExtension) {
+      responseType = SERVE;  // assume a serve request if file extension is present
+      if (clientAcceptsHtml(headers)) {  // if the request comes from a browser, change it to a show request
+        responseType = SHOW;
+      }
+    } else {
+      responseType = SHOW;
+      if (clientWantsAsset(headers) && requestIsFromBrowser(headers)) {  // this is in case someone embeds a show url
+        logger.debug('Show request came from browser but wants an image/video. Changing response to serve...');
+        responseType = SERVE;
+      }
+    }
+    return responseType;
   },
-  showFileLite (claimInfo, shortId, res) {
-    logger.verbose(`showlite claim: ${claimInfo.name}#${claimInfo.claimId}`);
-    res.status(200).render('index');
+  flipClaimNameAndIdForBackwardsCompatibility (identifier, name) {
+    // this is a patch for backwards compatability with '/name/claim_id' url format
+    if (isValidShortIdOrClaimId(name) && !isValidShortIdOrClaimId(identifier)) {
+      const tempName = name;
+      name = identifier;
+      identifier = tempName;
+    }
+    return [identifier, name];
+  },
+  logRequestData (responseType, claimName, channelName, claimId) {
+    logger.debug('responseType ===', responseType);
+    logger.debug('claim name === ', claimName);
+    logger.debug('channel name ===', channelName);
+    logger.debug('claim id ===', claimId);
   },
 };
diff --git a/helpers/statsHelpers.js b/helpers/statsHelpers.js
index f37e7df1..9e471172 100644
--- a/helpers/statsHelpers.js
+++ b/helpers/statsHelpers.js
@@ -1,22 +1,7 @@
-const constants = require('../constants');
 const logger = require('winston');
-const ua = require('universal-analytics');
-const config = require('../config/speechConfig.js');
-const googleApiKey = config.analytics.googleId;
 const db = require('../models');
 
 module.exports = {
-  createPublishTimingEventParams (publishDurration, ip, headers, label) {
-    return {
-      userTimingCategory    : 'lbrynet',
-      userTimingVariableName: 'publish',
-      userTimingTime        : publishDurration,
-      userTimingLabel       : label,
-      uip                   : ip,
-      ua                    : headers['user-agent'],
-      ul                    : headers['accept-language'],
-    };
-  },
   postToStats (action, url, ipAddress, name, claimId, result) {
     logger.debug('action:', action);
     // make sure the result is a string
@@ -50,46 +35,4 @@ module.exports = {
         logger.error('Sequelize error >>', error);
       });
   },
-  sendGoogleAnalyticsEvent (action, headers, ip, originalUrl) {
-    const visitorId = ip.replace(/\./g, '-');
-    const visitor = ua(googleApiKey, visitorId, { strictCidFormat: false, https: true });
-    let params;
-    switch (action) {
-      case 'SERVE':
-        params = {
-          ec : 'serve',
-          ea : originalUrl,
-          uip: ip,
-          ua : headers['user-agent'],
-          ul : headers['accept-language'],
-        };
-        break;
-      default: break;
-    }
-    visitor.event(params, (err) => {
-      if (err) {
-        logger.error('Google Analytics Event Error >>', err);
-      }
-    });
-  },
-  sendGoogleAnalyticsTiming (action, headers, ip, originalUrl, startTime, endTime) {
-    const visitorId = ip.replace(/\./g, '-');
-    const visitor = ua(googleApiKey, visitorId, { strictCidFormat: false, https: true });
-    const durration = endTime - startTime;
-    let params;
-    switch (action) {
-      case constants.PUBLISH_ANONYMOUS_CLAIM:
-      case constants.PUBLISH_IN_CHANNEL_CLAIM:
-        logger.verbose(`${action} completed successfully in ${durration}ms`);
-        params = module.exports.createPublishTimingEventParams(durration, ip, headers, action);
-        break;
-      default: break;
-    }
-    visitor.timing(params, (err) => {
-      if (err) {
-        logger.error('Google Analytics Event Error >>', err);
-      }
-      logger.debug(`${action} timing event successfully sent to google analytics`);
-    });
-  },
 };
diff --git a/routes/api-routes.js b/routes/api-routes.js
index 21e5b656..9178834d 100644
--- a/routes/api-routes.js
+++ b/routes/api-routes.js
@@ -5,9 +5,9 @@ const multipartMiddleware = multipart({uploadDir: files.uploadDirectory});
 const db = require('../models');
 const { checkClaimNameAvailability, checkChannelAvailability, publish } = require('../controllers/publishController.js');
 const { getClaimList, resolveUri, getClaim } = require('../helpers/lbryApi.js');
-const { createPublishParams, parsePublishApiRequestBody, parsePublishApiRequestFiles, parsePublishApiChannel, addGetResultsToFileData, createFileData, returnPublishTimingActionType } = require('../helpers/publishHelpers.js');
+const { createPublishParams, parsePublishApiRequestBody, parsePublishApiRequestFiles, parsePublishApiChannel, addGetResultsToFileData, createFileData } = require('../helpers/publishHelpers.js');
 const errorHandlers = require('../helpers/errorHandlers.js');
-const { sendGoogleAnalyticsTiming } = require('../helpers/statsHelpers.js');
+const { sendGAAnonymousPublishTiming, sendGAChannelPublishTiming } = require('../helpers/googleAnalytics.js');
 const { authenticateIfNoUserToken } = require('../auth/authentication.js');
 const { getChannelData, getChannelClaims, getClaimId } = require('../controllers/serveController.js');
 
@@ -134,12 +134,10 @@ module.exports = (app) => {
   app.post('/api/claim/publish', multipartMiddleware, ({ body, files, headers, ip, originalUrl, user }, res) => {
     logger.debug('api/claim-publish body:', body);
     logger.debug('api/claim-publish files:', files);
-    // record the start time of the request and create variable for storing the action type
-    const publishStartTime = Date.now();
-    logger.debug('publish request started @', publishStartTime);
-    let timingActionType;
     // define variables
     let  name, fileName, filePath, fileType, nsfw, license, title, description, thumbnail, channelName, channelPassword;
+    // record the start time of the request
+    const publishStartTime = Date.now();
     // validate the body and files of the request
     try {
       // validateApiPublishRequest(body, files);
@@ -166,8 +164,6 @@ module.exports = (app) => {
         return createPublishParams(filePath, name, title, description, license, nsfw, thumbnail, channelName);
       })
       .then(publishParams => {
-        // set the timing event type for reporting
-        timingActionType = returnPublishTimingActionType(publishParams.channel_name);
         // publish the asset
         return publish(publishParams, fileName, fileType);
       })
@@ -182,10 +178,13 @@ module.exports = (app) => {
             lbryTx : result,
           },
         });
-        // log the publish end time
+        // record the publish end time and send to google analytics
         const publishEndTime = Date.now();
-        logger.debug('publish request completed @', publishEndTime);
-        sendGoogleAnalyticsTiming(timingActionType, headers, ip, originalUrl, publishStartTime, publishEndTime);
+        if (channelName) {
+          sendGAChannelPublishTiming(headers, ip, originalUrl, publishStartTime, publishEndTime);
+        } else {
+          sendGAAnonymousPublishTiming(headers, ip, originalUrl, publishStartTime, publishEndTime);
+        }
       })
       .catch(error => {
         errorHandlers.handleErrorResponse(originalUrl, ip, error, res);
diff --git a/routes/serve-routes.js b/routes/serve-routes.js
index 38c755ee..9a1c303e 100644
--- a/routes/serve-routes.js
+++ b/routes/serve-routes.js
@@ -1,88 +1,9 @@
-const logger = require('winston');
-const { getClaimId, getLocalFileRecord } = require('../controllers/serveController.js');
-const serveHelpers = require('../helpers/serveHelpers.js');
-const { handleErrorResponse } = require('../helpers/errorHandlers.js');
+const { sendGAServeEvent } = require('../helpers/googleAnalytics');
+
+const { determineResponseType, flipClaimNameAndIdForBackwardsCompatibility, logRequestData, getClaimIdAndServeAsset } = require('../helpers/serveHelpers.js');
 const lbryUri = require('../helpers/lbryUri.js');
 
 const SERVE = 'SERVE';
-const SHOW = 'SHOW';
-const NO_CHANNEL = 'NO_CHANNEL';
-const NO_CLAIM = 'NO_CLAIM';
-const NO_FILE = 'NO_FILE';
-
-function isValidClaimId (claimId) {
-  return ((claimId.length === 40) && !/[^A-Za-z0-9]/g.test(claimId));
-}
-
-function isValidShortId (claimId) {
-  return claimId.length === 1;  // it should really evaluate the short url itself
-}
-
-function isValidShortIdOrClaimId (input) {
-  return (isValidClaimId(input) || isValidShortId(input));
-}
-
-function clientAcceptsHtml ({accept}) {
-  return accept && accept.match(/text\/html/);
-}
-
-function requestIsFromBrowser (headers) {
-  return headers['user-agent'] && headers['user-agent'].match(/Mozilla/);
-};
-
-function clientWantsAsset ({accept, range}) {
-  const imageIsWanted = accept && accept.match(/image\/.*/) && !accept.match(/text\/html/) && !accept.match(/text\/\*/);
-  const videoIsWanted = accept && range;
-  return imageIsWanted || videoIsWanted;
-}
-
-function determineResponseType (hasFileExtension, headers) {
-  let responseType;
-  if (hasFileExtension) {
-    responseType = SERVE;  // assume a serve request if file extension is present
-    if (clientAcceptsHtml(headers)) {  // if the request comes from a browser, change it to a show request
-      responseType = SHOW;
-    }
-  } else {
-    responseType = SHOW;
-    if (clientWantsAsset(headers) && requestIsFromBrowser(headers)) {  // this is in case someone embeds a show url
-      logger.debug('Show request came from browser but wants an image/video. Changing response to serve...');
-      responseType = SERVE;
-    }
-  }
-  return responseType;
-}
-
-function serveAssetToClient (claimId, name, res) {
-  return getLocalFileRecord(claimId, name)
-      .then(fileInfo => {
-        // logger.debug('fileInfo:', fileInfo);
-        if (fileInfo === NO_FILE) {
-          return res.status(307).redirect(`/api/claim/get/${name}/${claimId}`);
-        }
-        return serveHelpers.serveFile(fileInfo, claimId, name, res);
-      })
-      .catch(error => {
-        throw error;
-      });
-}
-
-function flipClaimNameAndIdForBackwardsCompatibility (identifier, name) {
-  // this is a patch for backwards compatability with '/name/claim_id' url format
-  if (isValidShortIdOrClaimId(name) && !isValidShortIdOrClaimId(identifier)) {
-    const tempName = name;
-    name = identifier;
-    identifier = tempName;
-  }
-  return [identifier, name];
-}
-
-function logRequestData (responseType, claimName, channelName, claimId) {
-  logger.debug('responseType ===', responseType);
-  logger.debug('claim name === ', claimName);
-  logger.debug('channel name ===', channelName);
-  logger.debug('claim id ===', claimId);
-}
 
 module.exports = (app) => {
   // route to serve a specific asset using the channel or claim id
@@ -98,6 +19,9 @@ module.exports = (app) => {
     if (responseType !== SERVE) {
       return res.status(200).render('index');
     }
+    // handle serve request
+    // send google analytics
+    sendGAServeEvent(headers, ip, originalUrl);
     // parse the claim
     let claimName;
     try {
@@ -118,20 +42,7 @@ module.exports = (app) => {
     // log the request data for debugging
     logRequestData(responseType, claimName, channelName, claimId);
     // get the claim Id and then serve the asset
-    getClaimId(channelName, channelClaimId, claimName, claimId)
-      .then(fullClaimId => {
-        if (fullClaimId === NO_CLAIM) {
-          return res.status(404).json({success: false, message: 'no claim id could be found'});
-        } else if (fullClaimId === NO_CHANNEL) {
-          return res.status(404).json({success: false, message: 'no channel id could be found'});
-        }
-        serveAssetToClient(fullClaimId, claimName, res);
-        // postToStats(responseType, originalUrl, ip, claimName, fullClaimId, 'success');
-      })
-      .catch(error => {
-        handleErrorResponse(originalUrl, ip, error, res);
-        // postToStats(responseType, originalUrl, ip, claimName, fullClaimId, 'fail');
-      });
+    getClaimIdAndServeAsset(channelName, channelClaimId, claimName, claimId, originalUrl, ip, res);
   });
   // route to serve the winning asset at a claim or a channel page
   app.get('/:claim', ({ headers, ip, originalUrl, params, query }, res) => {
@@ -146,6 +57,9 @@ module.exports = (app) => {
     if (responseType !== SERVE) {
       return res.status(200).render('index');
     }
+    // handle serve request
+    // send google analytics
+    sendGAServeEvent(headers, ip, originalUrl);
     // parse the claim
     let claimName;
     try {
@@ -156,17 +70,6 @@ module.exports = (app) => {
     // log the request data for debugging
     logRequestData(responseType, claimName, null, null);
     // get the claim Id and then serve the asset
-    getClaimId(null, null, claimName, null)
-      .then(fullClaimId => {
-        if (fullClaimId === NO_CLAIM) {
-          return res.status(404).json({success: false, message: 'no claim id could be found'});
-        }
-        serveAssetToClient(fullClaimId, claimName, res);
-        // postToStats(responseType, originalUrl, ip, claimName, fullClaimId, 'success');
-      })
-      .catch(error => {
-        handleErrorResponse(originalUrl, ip, error, res);
-        // postToStats(responseType, originalUrl, ip, claimName, fullClaimId, 'fail');
-      });
+    getClaimIdAndServeAsset(null, null, claimName, null, originalUrl, ip, res);
   });
 };