0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Aggregation APIのDSLを作った話

0
Last updated at Posted at 2026-01-08

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に寄せられるので、調査系のループがかなり楽になります。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?