JSONと戦うのに疲れたので、構造化DSLを作ってみた
Pendo の Aggregation API、強力だけど正直つらくないですか。
• ネストが深くなりがちなJSON
• 実行するまで syntax エラーに気づけない
• カンマ1個、キー1文字のミスでAPIエラー
• 集計が複雑になるほど可読性が落ちる
自分はこれにずっと使いづらさを感じていて、
「Pendo Aggregation APIを、JSON以外の形で書けないのか?」
「実行前にエラーが分かるようにできないのか?」
と思い、DSLを自作してみました。
aggdsl とは
aggdsl は Pendo Aggregation API 向けのDSL です。
JSONを直接書く代わりに、
• よりシンプルで
• 構造が分かりやすく
• 実行前に構文エラーを検出できる
構文で Aggregation を記述できます。
目指したのは次の3点です。
• Pendo Aggregation APIのJSONを書かなくていい
• syntax エラーを事前に検出できる
• ネスト構造が直感的に分かる
なぜ作ったか
JSONが人間の目に厳しい
Pendo Aggregation APIは、集計が複雑になるほどJSONのネストが深くなります。
今どの aggregation を書いているのか
どこでネストが終わっているのか
JSONだと目が痛い。
{
"response": {
"location": "request",
"mimeType": "application/json"
},
"request": {
"pipeline": [
{
"spawn": [
[
{
"source": {
"pageEvents": {
"blacklist": "apply"
},
"timeSeries": {
"period": "dayRange",
"first": "now()",
"count": -7
}
}
},
{
"merge": {
"fields": [
"pageId"
],
"pipeline": [
{
"source": {
"pages": {
"appId": []
}
}
},
{
"filter": "!isNil(group.id)"
},
{
"eval": {
"pageId": "id",
"groupId": "group.id",
"groupName": "group.name"
}
}
],
"mappings": {
"productAreaId": "groupId",
"productAreaName": "groupName"
}
}
},
{
"group": {
"group": [
"productAreaId",
"productAreaName"
],
"fields": [
{
"totalEvents": {
"sum": "numEvents"
}
},
{
"totalMinutes": {
"sum": "numMinutes"
}
}
]
}
},
{
"eval": {
"resultType": "`page`"
}
},
{
"sort": [
"-totalEvents"
]
}
],
[
{
"source": {
"featureEvents": {
"blacklist": "apply"
},
"timeSeries": {
"period": "dayRange",
"first": "now()",
"count": -7
}
}
},
{
"merge": {
"fields": [
"featureId"
],
"pipeline": [
{
"source": {
"features": {
"appId": []
}
}
},
{
"filter": "!isNil(group.id)"
},
{
"eval": {
"featureId": "id",
"groupId": "group.id",
"groupName": "group.name"
}
}
],
"mappings": {
"productAreaId": "groupId",
"productAreaName": "groupName"
}
}
},
{
"group": {
"group": [
"productAreaId",
"productAreaName"
],
"fields": [
{
"totalEvents": {
"sum": "numEvents"
}
},
{
"totalMinutes": {
"sum": "numMinutes"
}
}
]
}
},
{
"eval": {
"resultType": "`feature`"
}
},
{
"sort": [
"-totalEvents"
]
}
]
]
}
],
"name": "ProductAreas_PageAndFeatureUsage"
}
}
実行するまでエラーが分からないのがつらい
Pendo Aggregation APIあるあるだと思うんですが、
• 構文ミス
• 必須パラメータ不足
• 構造の間違い
これらは APIを叩くまで分からない。
「JSONを書いて → API叩いて → エラー見て → 修正する」のループが地味にストレスでした。
aggdsl の書き方イメージ
ポイントはこんな感じ。
• FROM event([...]), TIMESERIES ... などのソース指定をDSLで書ける 
• filter, identified, group, sort, limit といったパイプラインステージをサポート 
• spawn によるブランチ、switch による値マッピングもDSLのステージとして書ける 
• さらに JSON→DSL の逆変換(decompile) も用意して、JSONエクスポートなどを「編集可能な形」に戻せる 
- ソースが明確
- どのステージの処理であるかわかりやすい
- ブロックを|で表現
- JSONに変換する前に構文チェック可能
「Pendo Aggregationの構造を、そのままコードにした」感覚に近いです。
さっきのAggregationは下記のように表現されます。
REQUEST name="ProductAreas_PageAndFeatureUsage"
RESPONSE mimeType=application/json
PIPELINE
| spawn
branch
FROM event([source=pageEvents,blacklist="apply"])
TIMESERIES period=dayRange first=now() count=-7
|| merge fields [pageId] mappings { productAreaId=groupId, productAreaName=groupName }
FROM event([source=pages,appId=[]])
| filter !isNil(group.id)
| eval { pageId=id, groupId=group.id, groupName=group.name }
endmerge
|| group by productAreaId,productAreaName fields { totalEvents=sum(numEvents), totalMinutes=sum(numMinutes) }
|| eval { resultType=`page` }
|| sort -totalEvents
endbranch
branch
FROM event([source=featureEvents,blacklist="apply"])
TIMESERIES period=dayRange first=now() count=-7
|| merge fields [featureId] mappings { productAreaId=groupId, productAreaName=groupName }
FROM event([source=features,appId=[]])
| filter !isNil(group.id)
| eval { featureId=id, groupId=group.id, groupName=group.name }
endmerge
|| group by productAreaId,productAreaName fields { totalEvents=sum(numEvents), totalMinutes=sum(numMinutes) }
|| eval { resultType=`feature` }
|| sort -totalEvents
endbranch
| endspawn
もう一つの便利ポイント: eventソースから直接書き始められる
aggdsl は、いきなりeventソースを起点にしてDSLで書けます。
例えば、featureEvents をそのまま引いて、必要なフィールドだけ抜き出す場合。
FROM event([source=featureEvents])
TIMESERIES period=dayRange first=now() count=-1
| select {
featureId=featureId,
accountId=accountId,
visitorId=visitorId,
browserTime=browserTime
}
結果これにコンパイルされる
{
"response": {
"location": "request",
"mimeType": "application/json"
},
"request": {
"pipeline": [
{
"source": {
"featureEvents": {},
"timeSeries": {
"period": "dayRange",
"first": "now()",
"count": -1
}
}
},
{
"select": {
"featureId": "featureId",
"accountId": "accountId",
"visitorId": "visitorId",
"browserTime": "browserTime"
}
}
]
}
}
実行前にエラーが分かるのが一番のメリット
aggdsl では DSL をパースする段階でエラーを検出します。
• aggregation の書き方が間違っている
• 必須パラメータが足りない
• ネスト構造がおかしい
これらを Pendo APIを呼ぶ前に ある程度 検出できます。
「とりあえず投げてみる」から「書いた時点で安心できる」に変わります。
どんな人に向いているか
• Pendo Aggregation APIをよく書く人
• JSONのネストに毎回イライラしている人
• APIを叩く前にエラーを潰したい人
まとめ
Pendo Aggregation APIは強力ですが、JSONで書く体験は正直つらい。
だったら「人間が書きやすく、事前にエラーが分かるDSL」を作ろう、というのが aggdsl です。
まだ発展途上ですが、同じ悩みを持っている人には刺さると思っています。
• リポジトリ
https://github.com/Band-Aid/aggdsl
Issueやフィードバック、PRも大歓迎です。
小ネタ: VS CodeのAgent Skillsを入れたので、Copilotに集計を頼める
おまけで、VS Code の GitHub Copilot が読める Agent Skills もこのリポジトリに追加しました(.github/skills/... 配下)
Agent Skills は、Copilot が必要なときだけ読み込む「手順書 + スクリプト + リソース」一式みたいなもので、ドメイン固有の作業をエージェントに教えられます。 
これを使うと、Copilot に「このDSLをコンパイルしてPendo Aggregation APIを叩いて結果まとめて」みたいなお願いができます。
使い方ざっくり
• VS Code 側で Agent Skills を有効化(現状プレビュー扱いで、Insidersや設定が必要な場合あり) 
• Copilot Chat を Agent Mode で起動
• あとは自然言語で依頼
依頼例
• 「この query.dsl をコンパイルして、Pendo Aggregation API に投げて結果をJSONで見せて」
• 「直近7日で productArea 別の totalEvents 上位だけ表にして」
• 「集計結果の傾向を3行でまとめて、気になるところ追加で掘るクエリ案も出して」
DSL作成、実行と要約もCopilotに寄せられるので、調査系のループがかなり楽になります。