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, тег заменяет новым.
Если cookie пропала
- Если
_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 так делать не нужно.
Как проверить
- Откройте страницу сайта и в инструментах разработчика браузера найдите cookie
_dara_aи_dara_a_srv. Срок_dara_a— через 400 суток от сегодняшнего дня. - Удалите
_dara_a_srvи обновите страницу. Значение_dara_aдолжно остаться прежним, срок — снова 400 суток, а_dara_a_srv— появиться заново. - Посмотрите в заголовках ответа: вместе с
Set-Cookieдля_dara_aпришёлCache-Controlсprivateилиno-store. - Обновите страницу ещё раз:
Set-Cookieдля_dara_aбольше не приходит.