К содержанию
ActionPulse

Постраничность

Как читать большие списки ActionPulse API — непрозрачный курсор, параметры cursor и limit, признак последней страницы, отличия реестра и рабочий цикл обхода.

Кому
Разработчикам, читающим большие списки
Проверено
На этой странице

Списковые методы ActionPulse отдают данные страницами по курсору. Номеров страниц и смещения в API нет: вы получаете страницу и вместе с ней значение, которое нужно передать, чтобы получить следующую. Эта страница нужна, когда список не помещается в один ответ — сырые события, участники, сессии, записи реестра, журнал аудита. Какие именно параметры принимает конкретный метод, указано на его странице в справочнике API.

Параметры

Параметр Где Тип Границы По умолчанию
cursor строка запроса строка до 4096 символов нет — отсутствие означает «первая страница»
limit строка запроса целое от 1 до 100 50

Это общие для API значения. У части методов границы другие — см. таблицу исключений ниже. limit — верхняя граница размера страницы, а не гарантия: страница может оказаться короче.

В ответе значение для следующего шага приходит в поле next_cursor.

{
  "events": [{ "event_id": "8b0d3f2e-1c4a-4f0e-9b1d-2a3c4d5e6f70" }],
  "next_cursor": "b3BhcXVlLWN1cnNvci12YWx1ZQ"
}

Имя массива с данными у каждого метода своё — events, sessions, imports, violations, actors, records и так далее, — а поле с курсором во всём API называется одинаково: next_cursor. Поля элемента в примере сокращены.

Курсор непрозрачен

Курсор — не идентификатор последнего элемента и не номер строки. Это закодированное значение, в которое сервер укладывает позицию обхода и отпечаток всей области запроса: проект, фильтры, часовой пояс, вычисленные границы периода. Разбирать, собирать и править его вручную нельзя — формат не является частью контракта и может измениться.

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

Что изменили между страницами Результат
Фильтр, период, часовой пояс Курсор больше не подходит: 422 validation_failed, details.field=cursor
Проект в адресе То же: курсор привязан к проекту и в другом проекте не принимается
Взяли курсор из другого списка То же: курсоры разных методов не взаимозаменяемы
Только limit Допустимо: размер следующей страницы можно менять

Слишком длинное значение отклоняется до попытки его раскодировать — так метод не тратит работу на заведомо некорректный ввод.

Как понять, что страницы закончились

Единственный признак — поле next_cursor:

  • поля нет, оно пустое или null — данных больше нет, обход закончен;
  • поле непустое — есть ещё как минимум одна страница.

Останавливайтесь именно по нему, а не по числу элементов: контракт обещает next_cursor только как признак наличия следующих данных, а длина страницы признаком конца не является. Страница короче limit с непустым next_cursor — нормальный ответ.

Чего в ответе нет:

  • признака has_more — его роль полностью выполняет next_cursor;
  • общего количества элементов как универсального поля. У отдельных методов в ответе есть total — например, у выгрузки участников когорты retention и у списка тех, кто отвалился на шаге воронки, — но это часть их предметного результата, а не счётчик страниц. Рассчитывать на него в общем клиенте нельзя;
  • возможности перейти на страницу N. Обход только последовательный, с начала.

Порядок задаёт сам метод — у большинства списков от новых к старым; точный порядок указан в описании метода.

Курсор реестра

У реестра событий свой параметр курсора. Отличий два, и оба стоит учесть в общем клиенте:

  • длина — до 2048 символов вместо 4096. Если ваш код валидирует длину сам, он должен знать про меньшую границу;
  • область — курсор привязан к проекту и полному набору условий запроса реестра. У двух методов реестра — журнала нарушений и сводки состояния — границы from и to обязательны, задают непустой период не длиннее 31 дня (from включительно, to исключительно) и входят в область курсора. Сдвинули окно — начали новый обход.

limit у реестра общий: от 1 до 100, по умолчанию 50.

Где границы другие

Проверено по спецификации. Всё, чего в таблице нет, использует общие значения из первого раздела.

Метод cursor limit
Список сырых событий до 4096 1…100, значения по умолчанию нет — задайте его сами
Фрагменты записи сессии до 4096 1…16, по умолчанию 8
Маркеры релизов, токены релизов до 512 1…200, по умолчанию 100
События правила оповещения до 512 1…100, по умолчанию 100
Запуски еженедельной сводки до 1024 1…100, по умолчанию 20
Журнал аудита до 1024 1…200, по умолчанию 50
Доставки защищённой ссылки до 1024 1…100, по умолчанию 50
Методы реестра событий до 2048 1…100, по умолчанию 50
Сохранённые сегменты ограничение длины в схеме не объявлено 1…100, по умолчанию 50
Список задач импорта ограничение длины в схеме не объявлено 1…100, по умолчанию 50 — значения заданы только в описании параметра, не в схеме
Список задач удаления данных субъекта курсора нет: список ограниченный, а не постраничный 1…100, по умолчанию 50

Отдельный случай — методы, где курсор передаётся в теле POST-запроса, а не в строке запроса: выгрузка участников когорты retention, отчёт атрибуции, исторический прогон сезонного оповещения и список тех, кто отвалился на шаге воронки (там поля называются dropped_cursor и dropped_limit). Правила те же, отличается только место параметра; у сезонного прогона курсор короче — до 256 символов, а страница — от 1 до 50 элементов, по умолчанию 30.

Типичная ошибка: сохранённый курсор

Курсор выглядит как закладка, и его хочется положить в базу, чтобы «в следующий раз продолжить с того места». Так он не работает.

  • Списки упорядочены от новых к старым. Новые строки появляются перед сохранённой позицией, а не после неё, поэтому старый курсор продолжит уводить вас в глубь старых данных и никогда не покажет то, что пришло позже.
  • В курсор вложен отпечаток области запроса и версия его собственного формата. Изменилось хоть что-то — вы получите 422, а не «пустую страницу».

Правильный приём для регулярного дочитывания — не курсор, а явные границы периода. Запоминайте не курсор, а до какого момента вы уже обработали данные, и следующий раз запрашивайте новый период с начала. Курсор живёт внутри одного обхода и там же умирает.

Рабочий пример

Обход списка сырых событий за закрытый период. Останов — по отсутствию next_cursor, отказ по курсору обрабатывается отдельно.

curl

test -n "$YOUR_ACCESS_TOKEN" || { echo "YOUR_ACCESS_TOKEN is required" >&2; exit 1; }

base="https://YOUR_ACTIONPULSE_HOST/api/v1/projects/YOUR_PROJECT_ID/events"
window="from=2026-07-01T00:00:00Z&to=2026-07-08T00:00:00Z&limit=100"
cursor=""

while : ; do
  url="$base?$window"
  if [ -n "$cursor" ]; then
    url="$url&cursor=$(printf '%s' "$cursor" | jq -sRr @uri)"
  fi

  page=$(curl -sS -w '\n%{http_code}' \
    -H "Authorization: Bearer $YOUR_ACCESS_TOKEN" "$url")
  code=$(printf '%s\n' "$page" | tail -n 1)
  body=$(printf '%s\n' "$page" | sed '$d')

  if [ "$code" != "200" ]; then
    echo "page failed: $code $(printf '%s' "$body" | jq -r '.error.code')" >&2
    exit 1
  fi

  printf '%s' "$body" | jq -c '.events[]'
  cursor=$(printf '%s' "$body" | jq -r '.next_cursor // empty')
  [ -n "$cursor" ] || break
done

Node.js

const accessToken = process.env.YOUR_ACCESS_TOKEN;
if (!accessToken) throw new Error('YOUR_ACCESS_TOKEN is required');

const base = 'https://YOUR_ACTIONPULSE_HOST/api/v1/projects/YOUR_PROJECT_ID/events';
// Условия запроса фиксированы на весь обход: менять их между страницами нельзя.
const window = { from: '2026-07-01T00:00:00Z', to: '2026-07-08T00:00:00Z', limit: '100' };

let cursor;
let guard = 0;

do {
  const url = new URL(base);
  for (const [key, value] of Object.entries(window)) url.searchParams.set(key, value);
  if (cursor) url.searchParams.set('cursor', cursor);

  const response = await fetch(url, {
    headers: { Authorization: `Bearer ${accessToken}` },
  });

  if (!response.ok) {
    const { error } = await response.json();
    // 422 с details.field === 'cursor' — курсор больше не подходит к области
    // запроса; продолжать обход бессмысленно, нужен новый заход с первой страницы.
    throw new Error(`page failed: ${response.status} ${error?.code} ${error?.details?.field ?? ''}`);
  }

  const page = await response.json();
  for (const event of page.events) handle(event);

  // Единственный признак конца — пустой или отсутствующий next_cursor.
  cursor = page.next_cursor || undefined;
  guard += 1;
} while (cursor && guard < 1000);

Ограничение числа итераций в примере — не требование API, а страховка от бесконечного цикла из-за ошибки в собственном коде.

Что проверить в своём клиенте

  1. Обход останавливается по отсутствию next_cursor, а не по короткой странице и не по счётчику.
  2. Условия запроса собираются один раз до начала обхода и не меняются внутри.
  3. 422 с details.field = "cursor" обрабатывается как «начать заново», а не как повторяемая ошибка.
  4. limit задан явно там, где у метода нет значения по умолчанию.
  5. Между страницами есть обработка 429: обход большого списка — это серия запросов, и на неё действуют лимиты частоты.
  6. Для регулярного дочитывания сохраняется граница обработанного периода, а не курсор.
  7. Если нужен воспроизводимый результат — период закрыт, либо задача решается выгрузкой, а не обходом.