Pull to refresh
289.49
TINKOFF
IT’s Tinkoff — просто о сложном

Асинхронная добыча меток utm

Reading time 4 min
Views 8.1K
Недавно, в процессе разработки клиентской части веб-приложения, возникла необходимость определять метки рекламной кампании, приведшей пользователя на сайт.

Изначально, задача показалась весьма линейной — посмотреть тут, потом там, взять что-то по приоритету и передать дальше. Но в процессе выяснилось, что некоторые метки могут появляться асинхронно, и, следовательно, их нужно уметь «ждать».

Усложнение задачи привело к желанию упростить код, участвующий в ее решении.

На примере решения такой задачи, данный пост пытается показать, как проектирование и over engineering может помочь вам в разработке гибких и легко изменяемых приложений.

Условия задачи

Метки, которые мы будем искать — это метки UTM.

Источники меток — это те места, в которых мы ищем метки. По условию, они имеют приоритет поиска. В нашей задаче источниками являются:
  • параметры GET запроса;
  • cookies;
  • заголовки HTTP запроса типа document.referrer.

Алгоритм чтения меток зависит от приоритета источника и изначально выглядит так:



Решение

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

  1. репозиторий cookies;
  2. репозиторий данных HTTP запроса (GET параметры, document.location.pathname и тд);
  3. и, непосредственно, сам сервис получения меток.

Назовем их соответственно cookies, query и utm.

Если с функциональностью cookies и query все достаточно понятно, то как быть с utm? Стоит ли реализовать алгоритм получения меток непосредственно внутри utm или абстрагироваться, и вынести реализацию алгоритма за пределы сервиса?

Алгоритм можно сильно упростить, если:
  • ввести понятие абстрактного источника данных с единым интерфейсом;
  • разделить метки на обязательные и опциональные.



Соответственно источниками должны быть наши сервисы cookies и query.

Но как быть, если у cookies геттер значения печеньки называется getCookie, а у query геттер параметра — getQueryParameter?

Другими словами, нам понадобиться использовать паттерн адаптер.

В результате появятся следующие сервисы-обертки:

  1. cookies-utm-adapter — выполняет поиск и, если нужно, декодирование сохраненной метки в кукисах;
  2. query-utm-adapter — выполняет поиск в GET параметрах;
  3. query-utm-adapter-backside — выполняет поиск по косвенным признакам HTTP запроса.

Сервис utm будет иметь метод addSource, который принимает в себя объект с интерфейсом источника меток и приоритетом для этого источника. Так же конструктор сервиса принимает в себя объект javascript, расширяющий дефолтные настройки сервиса.

На данной диаграмме представлена связь сервиса utm с репозиторием cookies:



В конфиге сервисов все это выглядело бы так:

cookies:
  path: ‘/src/service/cookies/cookies.js’

query:
  path: ‘/src/service/query/query.js’

cookies-utm-adapter:
  path: ‘/src/service/utm/cookies-utm-adapter.js’
  deps:
    calls: [[‘setCookieService’, [‘@cookies’]]

query-utm-adapter:
  path: ‘/src/service/utm/query-utm-adapter.js’
  deps:
    calls: [[‘setQueryService’, [‘@query’]]

query-utm-adapter-backside:
  path: ‘src/service/utm/query-utm-adapter-backside.js’
  deps:
    calls: [[‘setQueryService’, [‘@query’]]

utm:
  path: ‘src/service/utm/utm.js’
  deps:
    args: [
         parameters: [
           name: ‘utm_source’
           required: true
         ,
           name: ‘utm_content’
           required: false
         ,
           name: 'utm_term'
           required: false
         ]
    ]
    calls: [
      [‘addSource’, [‘@cookies-utm-adapter’, 0]],
      [‘addSource’, [‘@query-utm-adapter’, 1]],
      [‘addSource’, [‘@query-utm-adapter-backside’, 2]]
    ]

* в данном примере конфиг представлен на coffeescript, символы @ означают ссылку на инстанс сервиса. Похожий формат конфигов используется в компоненте Symfony Dependency Injection.

Представим, что мы все это дело реализовали и закодили. Теперь все работает, но работает синхронно. Как же быть с «ожиданием» каких-то меток?

Async.js + jQuery.Deferred

Пару слов о реализации.

Выбранное структурное решение имеет два логических уровня:
  • логика опроса источников внутри сервиса utm;
  • логика адаптеров, которая может быть самой разной — от простого обращения к внедренному репозиторию, до самых изощренных способов получения и форматирования данных о метках.

Для реализации асинхронного решения нам нужно внести изменения, как минимум, на первом уровне.

На уровне сервиса utm мы поменяем реализацию цикла опроса источников:
  • цикл сделаем асинхронным при помощи библиотеки async.js, реализующей основные методы для коллекций в асинхронном стиле;
  • в ответ на вызов метода get у адаптера будем ожидать либо значение метки, либо обещание на ее значение (promise) — в случае, когда адаптеру нужно ее подождать или где-то запросить. Обработка результата оборачивается в метод $.when и, в случае успешного разрешения, вызывает callback функцию цикла от async.

На уровне адаптеров мы добавим возвращение promise для тех меток, которые стоит ждать. Например, кука __utmz, которая выставляется после инициализации библиотеки ga.js (analytics.js) и может позволить определить некоторые метки.

Заключение

Иногда, в начале проектирования решаемой задачи мы не всегда представляем себе ее сложность и все подводные камни. И вот в такие моменты хочется сделать максимально просто, но чрезмерное дробление кода наводит на мысли об over engineering и вообще немного пугает. В данном случае, «правильность» проектирования принесла свои плоды и значительно упростила дальнейшие модификации логики приложения.

Спасибо за внимание! Надеюсь, кому-нибудь поможет.
Tags:
Hubs:
+4
Comments 6
Comments Comments 6

Articles

Information

Website
www.tinkoff.ru
Registered
Founded
Employees
over 10,000 employees
Location
Россия