ДАРА АНАЛИТИКА
Разделы документации

Документация

Cookie и Safari

Какие cookie и хранилища браузера использует тег, почему Safari теряет посетителей через 7 дней и как продлить cookie _dara_a с сервера сайта — готовые фрагменты для PHP, nginx и Node.js.

Что хранит тег

Где Имя Значение Срок
домен сайта _dara_a id посетителя сайта, UUID v4 400 суток
домен сайта _dara_v id визита (UUID v4), время последней активности и ключ источника визита тайм-аут визита с последней активности, по умолчанию 30 минут
домен сайта _dara_off 1 — этот браузер не учитывается 400 суток, в Safari — 7 дней
analytics.daratech.ru dara_nid id браузера в сети ДАРА, UUID v4 400 суток
  • _dara_a ставит тег, только если такой cookie ещё нет. Атрибуты — Path=/, SameSite=Lax, на https — Secure. Если у домена сайта в настройках разрешены поддомены, cookie ставится на этот домен (Domain=.example.ru) и видна на всех его поддоменах; иначе — без атрибута Domain, только для текущего хоста. Копия id хранится в localStorage под ключом _dara_a.
  • _dara_v ставит тег с теми же атрибутами Path, Domain, SameSite и Secure, что у _dara_a, и продлевает при каждой активности. По ней вкладки одного браузера попадают в один визит, а по ключу источника тег замечает переход из нового источника.
  • _dara_off тег ставит с теми же атрибутами, когда вы открываете ссылку «не учитывать этот браузер» из настроек сайта, и удаляет по ссылке с #dara-on. Браузер с этой cookie хиты не отправляет. Safari удаляет её через 7 дней, как любую cookie из JavaScript: в Safari откройте ссылку снова.
  • dara_nid ставит сборщик analytics.daratech.ru в ответ на запросы отправки данных /c/…, если cookie ещё нет, с атрибутами Path=/c/; HttpOnly; Secure; SameSite=None. Это cookie стороннего сайта: Safari и Brave её блокируют, а Firefox хранит отдельно для каждого сайта.

Кроме cookie, тег использует sessionStorage вкладки:

  • _dara_q — пачка хитов, которую не удалось отправить из-за сбоя сети, до 200 событий. Она уйдёт со следующей страницы;
  • _dara_m — отметки текущего визита: хеши адресов страниц, для которых метаданные уже отправлены, и отметка, что сведения об устройстве уже отправлены, — чтобы не отправлять их повторно. Сами адреса и значения там не хранятся.

Значение _dara_a или id визита, которое не является UUID, тег заменяет новым.

  • Если _dara_a пропала, а id остался в localStorage, тег восстанавливает cookie с прежним id.
  • Если нет ни cookie, ни копии, тег выдаёт новый id. В отчётах это новый посетитель, и его визиты не связываются с прежними.

Safari

Safari ограничивает 7 днями срок cookie, записанных из JavaScript, а через 7 дней без визита очищает хранилища скриптов, включая localStorage. Посетитель, который вернулся через 8 дней, получит новый id и в отчётах станет новым посетителем.

Cookie, которую сайт выставил ответом своего сервера, Safari так не ограничивает. Поэтому сервер сайта может раз в сутки перевыставлять _dara_a с тем же значением — тогда cookie живёт 400 суток. Тег существующую _dara_a не перезаписывает, поэтому серверная cookie не сменится 7-дневной.

Правила перевыставления

  • Когда: в ответах на запросы HTML-страниц сайта, если _dara_a уже есть и её значение — UUID. Сервер не создаёт id сам: его выдаёт тег.
  • Как часто: не чаще раза в сутки. Для этого сервер ставит служебную cookie _dara_a_srv на сутки и не трогает _dara_a, пока она есть.
  • Атрибуты _dara_a: срок 400 суток (Max-Age=34560000), Path=/, Secure, SameSite=Lax. Без HttpOnly: тег читает эту cookie из JavaScript.
  • Атрибуты _dara_a_srv: значение 1, срок сутки (Max-Age=86400), Path=/, Secure, HttpOnly, SameSite=Lax.
  • Домен. Перевыставляйте cookie с тем же доменом, что у cookie тега, иначе в браузере окажутся две разные _dara_a. Если у домена сайта в настройках разрешены поддомены, передайте во фрагмент этот домен, например example.ru; иначе оставьте домен пустым. Проверить можно в инструментах разработчика браузера: у cookie _dara_a, которую поставил тег, в поле Domain будет .example.ru или имя текущего хоста. Если две _dara_a всё же появились, тег берёт ту, что совпадает с копией в localStorage.
  • Откуда: ставьте cookie в ответе самой страницы сайта, а не запросом к отдельному поддомену или стороннему сервису. Если ответ приходит с IP-адреса из другой сети, чем адрес сайта, Safari может сократить срок и серверной cookie.
  • Кэш. Ответ с этими Set-Cookie предназначен одному посетителю. Если его сохранит общий кэш — CDN, Varnish, nginx с proxy_cache, который не смотрит на Set-Cookie, — все посетители получат одну и ту же _dara_a и в отчётах сольются в одного. Поэтому фрагменты ниже добавляют к заголовку Cache-Control такого ответа private, если там ещё нет private или no-store, и убирают public; остальные директивы приложения, например no-cache или must-revalidate, сохраняются. Если CDN кэширует страницы, не глядя на заголовки, исключите из кэша ответы с Set-Cookie.
  • Одностраничные приложения. HTML такого приложения сервер отдаёт один раз за визит, и следующего ответа страницы может не быть неделю. Продлевайте cookie и в ответах API сайта на том же домене.

Фрагменты ниже рассчитаны на сайт, который работает по HTTPS.

PHP

Вызовите функцию при обработке HTML-страницы до начала вывода — после кода, который выставляет заголовки кэширования, если он есть:

<?php
/**
 * Продлевает cookie _dara_a ДАРА АНАЛИТИКИ до 400 суток ответом сервера — не чаще раза в сутки.
 * $domain — домен cookie тега, например 'example.ru', или пустая строка.
 */
function dara_renew_visitor_cookie(string $domain = ''): void
{
    $id = $_COOKIE['_dara_a'] ?? null;
    if (!is_string($id)
        || !preg_match('/\A[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}\z/i', $id)
        || isset($_COOKIE['_dara_a_srv'])
        || headers_sent()
    ) {
        return;
    }
    setcookie('_dara_a', $id, [
        'expires' => time() + 400 * 86400,
        'path' => '/',
        'domain' => $domain,
        'secure' => true,
        'httponly' => false, // тег читает cookie из JavaScript
        'samesite' => 'Lax',
    ]);
    setcookie('_dara_a_srv', '1', [
        'expires' => time() + 86400,
        'path' => '/',
        'secure' => true,
        'httponly' => true,
        'samesite' => 'Lax',
    ]);

    // Ответ с cookie посетителя не должен попасть в общий кэш, иначе её получат все посетители
    $directives = [];
    foreach (headers_list() as $header) {
        if (stripos($header, 'Cache-Control:') !== 0) {
            continue;
        }
        foreach (explode(',', substr($header, strlen('Cache-Control:'))) as $directive) {
            $directive = trim($directive);
            if ($directive !== '' && strcasecmp($directive, 'public') !== 0) {
                $directives[] = $directive;
            }
        }
    }
    if (!preg_grep('/\A(private|no-store)\b/i', $directives)) {
        $directives[] = 'private';
    }
    header('Cache-Control: ' . implode(', ', $directives));
}

dara_renew_visitor_cookie();

nginx

Фрагмент работает, даже если страницы отдаёт другой сервер за nginx: заголовки Set-Cookie приложения сохраняются, nginx добавляет свои. Cookie продлевается только в ответах с Content-Type text/html или application/json — HTML-страницах и API сайта, — поэтому картинки, стили и скрипты остаются без изменений.

# В блоке http {}
map "$cookie__dara_a_srv:$cookie__dara_a:$sent_http_content_type" $dara_a_renew {
    default "";
    "~*^:(?<dara_a_id>[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}):(text/html|application/json)"
        "_dara_a=$dara_a_id; Max-Age=34560000; Path=/; Secure; SameSite=Lax";
}
map $dara_a_renew $dara_a_renew_flag {
    ""      "";
    default "_dara_a_srv=1; Max-Age=86400; Path=/; Secure; HttpOnly; SameSite=Lax";
}
map $dara_a_renew $dara_a_renew_cache {
    ""      "";
    default "private";
}

# В блоке server {} или в location {}, через который идут HTML-страницы и API сайта
add_header Set-Cookie $dara_a_renew;
add_header Set-Cookie $dara_a_renew_flag;
add_header Cache-Control $dara_a_renew_cache;
  • Если у cookie тега есть домен, допишите его в строку _dara_a: ...; Path=/; Domain=example.ru; Secure; SameSite=Lax.
  • add_header с пустым значением заголовок не добавляет, поэтому ответы без _dara_a, ответы, где _dara_a_srv уже есть, и ответы с другим Content-Type остаются без изменений.
  • Если API сайта отдаёт JSON с другим типом, например application/vnd.api+json, добавьте его в скобки регулярного выражения через |.
  • Если приложение за nginx само отдаёт Cache-Control, ответ с cookie получит вторую строку этого заголовка — private. По стандарту HTTP строки одного заголовка объединяются, и private запрещает общим кэшам сохранять ответ. Если ваш кэш читает только первую строку, исключите ответы с Set-Cookie из кэша.
  • Директивы add_header из блока server не действуют в location, где есть свои add_header. Если вы добавляете фрагмент в location, повторите в нём заголовки уровня server, например заголовки безопасности.

Node.js

Функция без зависимостей работает с http.ServerResponse, а значит и с Express:

const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;

function readCookie(header, name) {
  for (const part of (header || '').split(';')) {
    const eq = part.indexOf('=');
    if (eq > 0 && part.slice(0, eq).trim() === name) {
      return part.slice(eq + 1).trim();
    }
  }
  return null;
}

// Продлевает cookie _dara_a до 400 суток — не чаще раза в сутки.
// domain — домен cookie тега, например 'example.ru', или пустая строка.
function renewDaraCookie(req, res, domain = '') {
  const header = req.headers.cookie;
  const id = readCookie(header, '_dara_a');
  if (!id || !UUID.test(id) || readCookie(header, '_dara_a_srv') !== null || res.headersSent) {
    return;
  }
  const scope = domain ? `; Domain=${domain}` : '';
  const cookies = [
    `_dara_a=${id}; Max-Age=34560000; Path=/${scope}; Secure; SameSite=Lax`,
    '_dara_a_srv=1; Max-Age=86400; Path=/; Secure; HttpOnly; SameSite=Lax',
  ];
  const previous = res.getHeader('Set-Cookie');
  res.setHeader('Set-Cookie', previous === undefined ? cookies : [].concat(previous, cookies));
  // Ответ с cookie посетителя не должен попасть в общий кэш, иначе её получат все посетители
  const directives = String(res.getHeader('Cache-Control') || '')
    .split(',')
    .map((directive) => directive.trim())
    .filter((directive) => directive !== '' && directive.toLowerCase() !== 'public');
  if (!directives.some((directive) => /^(private|no-store)\b/i.test(directive))) {
    directives.push('private');
  }
  res.setHeader('Cache-Control', directives.join(', '));
}

module.exports = { renewDaraCookie };

Подключение в Express — для переходов на HTML-страницы и для API одностраничного приложения на том же домене. Картинки, стили и скрипты браузер запрашивает без text/html в Accept и без Sec-Fetch-Dest: document, поэтому статические файлы cookie и private не получают:

const express = require('express');
const { renewDaraCookie } = require('./dara-cookie');

const app = express();

app.use((req, res, next) => {
  const page = req.method === 'GET'
    && (req.get('Sec-Fetch-Dest') === 'document' || /text\/html/i.test(req.get('Accept') || ''));
  if (page || req.path.startsWith('/api/')) {
    renewDaraCookie(req, res);
  }
  next();
});

Подключайте обработчик до express.static и маршрутов. Если обработчик маршрута позже сам выставляет Cache-Control, он заменит значение с private — для ответов с Set-Cookie так делать не нужно.

Как проверить

  1. Откройте страницу сайта и в инструментах разработчика браузера найдите cookie _dara_a и _dara_a_srv. Срок _dara_a — через 400 суток от сегодняшнего дня.
  2. Удалите _dara_a_srv и обновите страницу. Значение _dara_a должно остаться прежним, срок — снова 400 суток, а _dara_a_srv — появиться заново.
  3. Посмотрите в заголовках ответа: вместе с Set-Cookie для _dara_a пришёл Cache-Control с private или no-store.
  4. Обновите страницу ещё раз: Set-Cookie для _dara_a больше не приходит.