Не тот ключ или не тот origin
Семь причин, по которым приём отвечает 401 или 403 — перепутанные ключи, чужой проект, устаревший префикс, отзыв, origin и список событий, — и как найти причину без подсказки в ответе.
На этой странице
- Быстрое различение
- Перепутанные ключи
- Серверный ключ попал в браузер
- Браузерный токен используется на бэкенде
- Ключ из другого проекта
- Ключ больше не тот, что нужен
- Устаревший префикс ключа
- Ключ отозван или истёк
- Ограничения браузерного токена
- Origin не в списке разрешённых
- Событие вне разрешённой области действия
- Почему в ответе не видно, что не совпало
- Как диагностировать без подсказки
- Что проверить
Приём событий отвечает 401 и 403 намеренно скупо: одно и то же сообщение для
разных причин, никакого эха присланного значения и никакой подсказки, что именно
не совпало. Это защита от подбора ключей — и одновременно то, что делает
диагностику неочевидной.
Ниже — семь причин, которые дают эти два кода, каждая со своим точным кодом ошибки и способом проверки. В конце — как выяснить причину, когда ответ ничего не подсказывает. Что вообще умеет каждый тип ключа, описано в типах ключей приёма.
Быстрое различение
| Что видно | Причина | Разбор |
|---|---|---|
| Запросов нет вовсе, консоль молчит | серверный ключ в теге | серверный ключ в браузере |
403 credential_origin_forbidden с бэкенда |
браузерный токен на сервере | браузерный токен на бэкенде |
202, событий нет ни на одном экране |
ключ другого проекта | ключ из другого проекта |
Всё работает, но в ответе Deprecation: true |
устаревший префикс | устаревший префикс |
401 unauthorized внезапно, без изменений в коде |
отзыв или истечение срока | ключ отозван или истёк |
403 credential_origin_forbidden из браузера |
origin не в списке | origin не в списке |
202 с credential_event_forbidden |
имя события не разрешено ключу | событие вне области действия |
Перепутанные ключи
Серверный ключ попал в браузер
Вероятная причина. В тег или в вызов инициализации подставлено значение с
префиксом pp_sk_. Трекер такое значение не принимает: он
проверяет форму credential до запуска и допускает только
pp_bt_ и pp_wk_. В режиме тега автозапуск
молча ничего не делает — ошибки в консоли нет. В сборке из пакета
инициализация бросает исключение.
Как проверить. В сетевой панели нет ни запроса /v1/cfg, ни запросов на
/v1/track, а window.pp при этом определён. Найдите префикс серверного
ключа в отданной браузеру разметке и в бандле:
grep -rn "pp_sk_" dist/ public/
Проверьте и контейнер тег-менеджера — в него значение попадает мимо репозитория.
Безопасное исправление. Выпустите браузерный токен с нужным списком origin и подставьте его. Серверный ключ верните на бэкенд — в переменную окружения. Затем отзовите утёкший ключ: он уже был виден каждому посетителю страницы, и «переставить его обратно» недостаточно.
Если не помогло. Проверьте, не указаны ли в теге сразу data-token и
data-key с разными значениями: в этом случае автозапуск тоже молча
отказывается работать. Оставьте один атрибут.
Браузерный токен используется на бэкенде
Вероятная причина. У запроса с сервера нет заголовка Origin, а браузерный
токен обязан пройти проверку точного origin. Пустой или непрозрачный origin не
совпадает ни с чем, поэтому запрос получает 403
credential_origin_forbidden — на /v1/track и /v1/identify. На
/v1/alias ответ будет другим: 403 credential_scope_forbidden, потому что
браузерному токену связывание идентификаторов недоступно вообще.
Как проверить. Посмотрите префикс типа в значении, которое читает бэкенд:
printf '%s\n' "${ACTIONPULSE_SERVER_KEY:0:6}"
Ожидаемо pp_sk_. Если там pp_bt_ — причина
найдена. Второй признак: тот же ключ из браузера работает, а с сервера — нет.
Безопасное исправление. Выпустите серверный ключ и переведите бэкенд на
него. Не пытайтесь «починить» это подстановкой заголовка Origin в серверный
запрос: проверка сработает, но вы получите публичный ключ в роли секретного, с
чужими ограничениями и сроком.
Если не помогло. Убедитесь, что бэкенд не отправляет подтверждённые
бизнес-факты браузерным токеном: payment_succeeded, order_paid,
registered и подобные принимаются только с серверным ключом, и отказ по ним
приходит поэлементно внутри 202, а не как 403.
Ключ из другого проекта
Вероятная причина. Ключ действителен, но принадлежит другому проекту той же
организации — обычно после копирования сниппета со стенда или из другого
окружения. Отказа не будет: ответ 202, события приняты — просто
в чужой проект. Проект определяется ключом, а не чем-либо в запросе.
Как проверить. Возьмите префикс ключа — 8 символов между вторым и третьим подчёркиванием — и поищите его в списке «Credentials проекта» настроек проекта. Если префикса там нет, откройте тот же список в других проектах: он найдётся там. Второй признак — события видны на экране проверки интеграции чужого проекта.
Безопасное исправление. Замените значение на ключ нужного проекта и перепроверьте, что события появились там, где ожидаются. Ключ чужого проекта отзывать не нужно — он не скомпрометирован, он просто не тот.
Если не помогло. События, уже принятые чужим проектом, остаются там: переносить события между проектами ActionPulse не умеет. Доступные варианты — удаление данных субъекта по идентификатору пользователя (роль администратора, см. приватность) либо ожидание срока хранения. Если чужой проект был создан только для теста, его можно удалить целиком вместе с данными.
Ключ больше не тот, что нужен
Устаревший префикс ключа
Вероятная причина. Используется ключ с префиксом pp_wk_ —
такие выпускались до разделения типов. Они продолжают работать, но несут старый
широкий профиль: и публичные, и секретные права сразу, без привязки к origin и
без срока. Признак в ответе — заголовок Deprecation: true, но только когда
ключ передан в адресе запроса.
Как проверить. Посмотрите тип в списке credentials проекта — устаревшие ключи помечены отдельно. В коде проверьте, не уходит ли ключ в адресе:
grep -rn "?key=" dist/ public/ src/
Безопасное исправление. Порядок, который не теряет события:
- выпустите браузерный токен со всеми origin и всеми своими именами событий, которые отправляются из браузера;
- замените значение в теге на новый токен, атрибут
data-keyуберите; - выпустите серверный ключ и переведите бэкенд на него;
- убедитесь, что события идут с обеих сторон;
- отзовите устаревший ключ.
Если не помогло. Ротация устаревшего ключа сохраняет его тип: получится новое устаревшее значение, а не браузерный или серверный ключ. Для перехода нужен именно выпуск ключа нужного типа.
Ключ отозван или истёк
Вероятная причина. 401 с кодом unauthorized и сообщением
invalid credential приходит и на отозванный, и на истёкший ключ — сообщение
одинаковое. Браузерный токен обязан иметь срок, по умолчанию это 90 дней с
выпуска, поэтому «внезапный 401 без изменений в коде» чаще всего означает
именно истечение.
Как проверить. В списке credentials проекта посмотрите столбцы «Истекает» и «Статус» у нужного префикса. Проверить сам факт приёма ключа можно безопасным запросом: авторизация выполняется до разбора тела, поэтому пустой пакет ничего не запишет.
curl -sS -o /dev/null -w '%{http_code}\n' \
-X POST "https://YOUR_ACTIONPULSE_INGEST_HOST/v1/track" \
-H "Authorization: Bearer $ACTIONPULSE_SERVER_KEY" \
-d '{"batch":[]}'
422 — ключ принят; 401 — нет.
Безопасное исправление. Истёкший токен ротировать нельзя — выпустите новый и замените значение. Для ключа, который вы отозвали случайно, восстановления тоже нет: нужен новый ключ. Ротация помогает только в одном случае — когда значение попало в чужие руки, а профиль ключа менять не нужно.
Если не помогло. Учтите хвост отзыва: обычно отозванный ключ перестаёт
приниматься сразу, но если внутренний канал мгновенного оповещения недоступен,
отказ наступает по истечении кэша — в пределах пяти минут. Обратная ситуация
тоже возможна: только что выпущенный ключ проверяется полностью, и резкий старт
большого потока сразу после выпуска может получить 429. Прогрейте ключ одним
запросом.
Ограничения браузерного токена
Origin не в списке разрешённых
Вероятная причина. Браузерный токен проверяет заголовок Origin на точное
совпадение со списком, заданным при выпуске. Маски не поддерживаются; схема,
поддомен и непустой порт входят в origin; пустой список означает отказ всегда.
Ответ — 403 credential_origin_forbidden.
Регистр, завершающий слеш, завершающая точка в имени хоста и порт по умолчанию
(80 для http, 443 для https) приводятся к одному виду и на результат не
влияют.
Как проверить. Во вкладке «Сеть» откройте неудачный запрос и посмотрите
заголовок запроса Origin — это значение, с которым идёт сравнение. Затем
сверьте его со столбцом «Scopes / origins» в списке credentials проекта. Тот же
отказ воспроизводится из терминала:
curl -sS -o /dev/null -w '%{http_code}\n' \
-X POST "https://YOUR_ACTIONPULSE_INGEST_HOST/v1/track" \
-H "Origin: https://example.com" \
-H "Authorization: Bearer YOUR_BROWSER_TOKEN" \
-d '{"batch":[]}'
Безопасное исправление. Список origin после выпуска не редактируется — ни
правкой, ни ротацией. Выпустите новый токен, перечислив все адреса сразу (от 1
до 32): production, варианты с www и без, поддомены со своими страницами,
адрес предпросмотра сборок. Замените значение, проверьте приём и затем отзовите
старый токен.
Если не помогло. Посмотрите, не приходит ли Origin: null — так выглядят
непрозрачные origin: страница из локального файла, песочница iframe,
документ из data:-адреса. Такой запрос не совпадёт ни с одним адресом.
Различение ошибки CORS и этого отказа разобрано в
блокировках из браузера.
Событие вне разрешённой области действия
Вероятная причина. Ключ принят, но конкретное имя события ему не разрешено.
Отказ приходит поэлементно внутри 202:
{ "accepted": 0, "n": 1, "rejected": [{ "i": 0, "code": "credential_event_forbidden" }] }
Причин у этого кода четыре:
| Ситуация | Что делать |
|---|---|
| Своё имя события не входит в список токена | выпустить токен с нужным списком или отправлять с сервера |
| Подтверждённый бизнес-факт отправляется из браузера | отправлять серверным ключом |
Имя из пространства pp.* отправляется серверным ключом |
это пространство формирует браузерный сбор |
| В запросе заявлен внутренний источник | убрать X-PP-Source: internal и context.source, иначе отклоняется каждый элемент пакета |
Только что выпущенный браузерный токен разрешает ровно одно своё имя —
signup_completed. Список задаётся при выпуске: от 1 до 128 имён, каждое по
маске ^[a-z0-9_.]{1,128}$, и ни одно не может начинаться с pp.. Встроенные
события pp.* списком не ограничены и работают независимо от него.
Как проверить. Сверьте имя события из rejected со списком имён у токена и
с реестром событий проекта. На экране проверки интеграции отказ виден вместе с
именем события, что быстрее, чем читать ответы вручную.
Безопасное исправление. Для сигналов интерфейса — выпустите токен с полным списком своих имён. Для подтверждённых фактов — переносите отправку на бэкенд: это не настройка, а граница доверия, и обойти её списком нельзя.
Если не помогло. Проверьте источник события в реестре проекта: если событие объявлено серверным, браузерный токен его не отправит даже при наличии имени в своём списке — реестр авторитетнее списка токена. Разбор остальных поэлементных отказов — в лимитах приёма.
Почему в ответе не видно, что не совпало
Это сделано намеренно, и знать про это полезно, чтобы не искать подсказку там, где её нет:
- сообщение при
401одинаково для всех причин — иначе по ответам можно было бы перебором отделять существующие ключи от несуществующих, а отозванные от истёкших; - ни один ответ не возвращает присланное значение ключа и не эхом повторяет присланный origin — иначе ключ утекал бы в логи, трассировки и отчёты об ошибках;
- публичная конфигурация сбора отвечает
200и заведомо безопасным «всё выключено» для неизвестного, отозванного, истёкшего и не подходящего по типу префикса — существование ключа не раскрывается; - поэлементный отказ несёт только индекс и код, без объяснения, какое именно правило сработало.
Как диагностировать без подсказки
Работайте не с ответом, а с двумя источниками, которые у вас есть: списком credentials проекта и префиксом ключа.
Префикс — это идентификатор. 8 символов между вторым и третьим подчёркиванием не секретны: по ним ключ находят в списке проекта, их можно писать в тикет и в логи. Секрет — только остаток значения.
Столбец «Использован» решает половину вопросов. Отметка обновляется при каждой успешной аутентификации ключа — в том числе тогда, когда запрос потом отклонён по области действия или по origin. Отсюда правило:
| Отметка «Использован» | Вывод |
|---|---|
Обновляется, а запросы получают 403 |
значение ключа верное, не совпадает политика: тип, область действия или origin |
Не обновляется, запросы получают 401 |
до проверки политики дело не дошло: значение не то, отозвано или истекло |
| Пусто, ключ выпущен давно | этим ключом не пользовались ни разу — вы смотрите не на тот ключ |
Отметка отстаёт до минуты, поэтому сравнивайте после небольшой паузы, а не мгновенно.
Проверка конфигурации по префиксу. Для браузерного и устаревшего ключа конфигурацию сбора можно запросить публично, передав только префикс — 8 символов, а не всё значение:
curl -sS "https://YOUR_ACTIONPULSE_INGEST_HOST/v1/cfg?key=YOURPREF" \
-H "Origin: https://example.com"
Если в настройках проекта хоть один переключатель сбора включён, а ответ приходит полностью выключенным — значит префикс не опознан или origin не совпал. Если же переключатели и так выключены, проверка ничего не покажет: включите один на время диагностики.
Пустой пакет как проба. Запрос с телом {"batch":[]} проходит авторизацию,
но ничего не записывает: 422 означает «ключ и политика в порядке», а любой
другой код указывает слой, на котором отказ произошёл. Это самая безопасная
проба на продакшене.
Что проверить
- В разметке и в бандле нет префикса серверного ключа — проверены и репозиторий, и контейнер тег-менеджера.
- Бэкенд читает ключ с префиксом
pp_sk_из переменной окружения, а не браузерный токен из конфига фронтенда. - Префикс из тега и префикс из бэкенда оба присутствуют в списке credentials нужного проекта.
- У браузерного токена в списке origin есть все рабочие адреса, а в календаре есть напоминание о его сроке.
- Список своих имён событий у токена совпадает с тем, что действительно вызывает фронтенд.
- В логи вашего приложения попадают код ошибки и префикс, но никогда само значение ключа.