Парсим Google Analytics на GO #1

GO это уже даже не "стильно модно молодежно", а просто норма. Он постепенно становится стандартом для написания демонов и других мелких и больших системных утилит. На нем пишут апи, которые обрабатывают кучу запросов и многое другое.

Ссылки на базовые туториалы и другие классные статьи "как начать писать" проще найти в гугле. Здесь же опишу задачу и решение.

Задача

Написать парсер данных из Google Analytics. В первом приближении вытащить только просмотры (pageViews) у заданного массива ссылок.

Прототип

  1. Авторизуемся через OAuth2 в Google
  2. Запрашиваем список доступных View из аналитики
  3. Выбираем нужный View
  4. Получаем от пользователя список ссылок и даты начала и конца поиска

Проблемы по пути

  • Никогда ничего не писал на GO
  • Уродливая документация Google Analytics
  • Библиотеки от Google без нормальных примеров использования

Сразу предупреждаю, код не претендует на красоту и гениальность, это просто пробный заход, накиданный за пару вечеров.

Начало

Установить GO. Ставил через Homebrew версию 1.6.2 – последняя доступная на момент написания статьи.

Далее нужно создать приложение в Google Developers Console. Включить для него Analytics API и Analytics Reporting API V4. Для тестов callback url можно указать на localhost. У меня было указано localhost:3000.

Сохранить куда-то ClientID и ClientSecret. Их можно найти в учетных данных во вкладке слева.

Подготовительные действия закончены, можно приступать к написанию кода.

Для авторизации надо поднять простенький http сервер и сделать несколько страниц. Начать можно со статьи на golang.org – Writing Web Applications. Приятно удивила обширность функционала в стандартных пакетах. Для большинства задач можно не импортировать сторонние библиотеки, а использовать только базовые пакеты и утилиты в комплекте с языком.

Отдельная боль – пакетный менеджер. Его просто нет. На Github можно найти список пакетных менеджеров сторонних разработчиков и выбрать на свой вкус. В статье не буду подробно на нем останавливаться, потому что не использовал и не могу толком сказать какие плюсы и минусы у каждого. С кем я общался – советуют gvt.

К коду. Простой http сервер:

package main

import (
  "net/http"
)

func indexHandler(w http.ResponseWriter, r *http.Request) {
  fmt.Frintf(w, "Hello from Index")
}

func main() {
  http.HandleFunc("/", indexHandler)
  http.HandleAndServe(":3000", nil)
}

Слушаем порт 3000 и по адресу http://localhost:3000/ – отдаем текстом Hello from Index.

Для OAuth 2.0 нам нужно сделать редирект на страницу в Google API, чтобы пользователь разрешил приложению делать разные манипуляции с данными и получить обратно в запросе access_token, который будем использовать в запросах к API.

func authHandler(w http.ResponseWriter, r *http.Request) {
}

func authHandler(w http.ResponseWriter, r *http.Request) {
}

func main() {
  http.HandleFunc("/authorize", authHandler)
  http.HandleFunc("/connect", connectHandler)

  http.HandleFunc("/", indexHandler)
  http.HandleAndServe(":3000", nil)
}

На главной странице выведем ссылку на /authorize, откуда сделаем редирект в Google, чтобы пользователь выдал нам права. На /connect будем ждать ответа от Google с кодом авторизации, по которому получим access_token.

После получения токена нужно его куда-то сохранить, для этого вполне сойдет сессия. В production такое делать не стоит и надо использовать базу, но я накидывал прототип, поэтому сессии подойдут.

Для работы с токеном будем пользоваться библиотекой от Google oauth2, а для работы с сессиями библиотекой sessions из GorillaToolkit.

Напомню, что в этой статье не описана начальная настройка окружения GO, предполагаю, что с этим можно справиться самостоятельно после прочтения нескольких статей. Поэтому укачиваем библиотеки себе в workspace с помощью команд:

go get github.com/gorilla/sessions
go get golang.org/x/oauth2

Импортируем их в проекте.

import(
  "net/http"
  "github.com/gorilla/sessions"
  "golang.rog/x/oauth2"
  "golang.rog/x/oauth2/google"
)

...

В библиотеках есть методы для получения конфига из json файла, но для прототипа можно забить это все руками.

import(
 ...
)

const (
  clientID        = "your_client_id"
  clientSecret    = "your_client_secret"
  applicationName = "your_application_name"
)

var store = sessions.NewCookieStore([]byte("gaIntegrationStore"))

var config = &oauth2.Config{
  ClientID: clientID,
  ClientSecret: clientSecret,
  Scopes: []string{"https://www.googleapis.com/auth/analytics", "https://www.googleapis.com/auth/analytics.readonly"},
  Endpoint: google.Endpoint,
  RedirectUrl: "http://localhost:3000/connect",
}

oauth2 – это набор методов для работы с OAuth 2.0 авторизацией, а в папках репозитория лежат в основном Endpoints для разных провайдеров – Google, Slack и т.д..
Сессии мы делаем через cookie без сохранения где-либо еще. При желании их можно хранить в memcache, redis и т.д..

Далее разберемся с шаблонами и отрендерим простую страницу с ссылкой для авторизации. Для этого положим в папку с проектом файл index.html:

<html>
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
    <meta http-equiv="x-ua-compatible" content="ie=edge">

    <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0-alpha.2/css/bootstrap.min.css" integrity="sha384-y3tfxAZXuh4HwSYylfB+J125MxIs6mR5FOHamPBG064zB+AFeWH94NdvaCBm8qnd" crossorigin="anonymous">
  </head>
  <body>
    <div class="container">
      <div class="row">
        <div class="col-sm-4">
          <a href="/authorize" class="btn btn-primary"> Authorize GA </a>
        </div>
        <div class="col-sm-4">
        </div>
        <div class="col-sm-4">
        </div>
      </div>
    </div>
    <script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.4/jquery.min.js"></script>
    <script src="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0-alpha.2/js/bootstrap.min.js" integrity="sha384-vZ2WRJMwsjRMW/8U7i6PWi6AlO1L79snBrmgiDpgIWJ82z8eA5lenwvxbMV1PAh7" crossorigin="anonymous"></script>
  </body>
</html>

Естественно подключили вездесущий Bootstrap. Далее отрендерим шаблон, обратимся к сессии и сделаем логирование:

import(
  "log"
  "html/template"
  ...
)

...

func indexHandler(w http.ResponseWriter, r *http.Request) {
  session, err := store.Get(r, "GASession")
  if err != nil {
    log.Println("IH.Error while retrieving session: ", err)
  }
  log.Println("Session: ", session)


  t, _ := template.ParseFiles("index.html")
  t.Execute(w, struct{}{})
}

Что мы сделали – получили сессию, считали index.html и отрендерили страницу. С сессией пока ничего не делаем, понадобится дальше.

Теперь надо отправить пользователя на страницу сервиса Google, где он даст нашему приложения необходимые права.

func authorizeHandler(w http.ResponseWriter, r *http.Request) {
  url := config.AuthCodeURL("state")
  http.Redirect(w, r, url, http.StatusFound)
}

После того как пользователь нажмет кнопку "Allow", сервис переправит его на урл, который мы указали в настройках /connect, поэтому напишу реализацию обработчика этого действия.

func connectHandler(w http.ResponseWriter, r *http.Request) {
  code := r.FormValue("code")
  log.Println("AH.Code: ", code)

  tok, err := config.Exchange(oauth2.NoContext, code)
  if err != nil {
    log.Fatalln("AH.Error: ", err)
  }
  log.Println("AH.AccessToken: ", tok)

  session, err := store.Get(r, "GASession")
  if err != nil {
    log.Println("AH.Error :", err)
  }
  session.Values["access_token"] = tok
  session.Save(r, w)

  http.Redirect(w, r, "/", http.StatusFound)
}

Разберу подробнее.

code := r.FormValue("code")

Когда пользователя средиректит обратно к нам, в параметрах запроса в ссылке будет поле code – проверочный код, по которому мы получим access_token с помощью метода Exchange из библиотеки oauth2.

tok, err := config.Exchange(oauth2.NoContext, code)

К сожалению складывать все подряд в объект сессии у нас не получиться. Чтобы сохранить там что-то отличное от стандартных типов нужно зарегистрировать тип, который мы хотим сохранить. Регистрируется он в функции init(). Она вызывается после всех определений констант и переменных и до вызова функции main().

func init() {
  gob.Register(&oauth2.Token{})
}

Далее сохраним объект сессии до отправки ответа:

func connectHandler(w http.ResponseWriter, r *http.Request) {
  ...
  session.Values["access_token"] = tok
  session.Save(r, w)
  log.Println("AH.Session: ", session)

  http.Redirect(w, r, "/", http.StatusFound)
}

Теперь в indexHandler мы можем обратиться к сессии и вытащить access_token для дальнейшей работы. В других обработчиках мы также можем обращаться к access_token и использовать его для инициализации клиента для запросов в API Google Analytics.

На этом закончу первую часть. Во второй расскажу как использовать библиотеку google.golang.org/api/ для запросов в API Google.