[bug-hunter] Heartbeat api monitor expands dotted params keys instead of preserving literals
- Dominant language
- Go
- Stars
- 12.7k
- Forks
- 5k
- Avg merge
- 2d 15m
- Merged PRs (30d)
- 385
Description
## Impact
`x-pack` Heartbeat `type: api` monitors that pass dotted `params` keys (for example `"subdomain.example.com"`) do not receive the original literal key. The key is dot-expanded into nested objects, which breaks journey code expecting `params["subdomain.example.com"]`.
This is user-impacting because `api` monitors reuse the browser source-job/params pipeline, but only `browser` streams get dotted-key preservation during config transform.
## Reproduction Steps
1. In `/home/runner/work/beats/beats/x-pack/heartbeat/cmd`, add this new test file:
```go
package cmd
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"google.golang.org/protobuf/types/known/structpb"
"github.com/elastic/elastic-agent-client/v7/pkg/client"
"github.com/elastic/elastic-agent-client/v7/pkg/proto"
)
func TestReproAPIParamsDottedKeys(t *testing.T) {
var rawIn proto.UnitExpectedConfig
err := readRawIn("testdata/simple-browser.in.json", &rawIn)
require.NoError(t, err, "failed to read fixture")
sourceMap := rawIn.GetSource().AsMap()
streams, ok := sourceMap["streams"].([]interface{})
require.True(t, ok, "expected streams slice")
require.NotEmpty(t, streams, "expected at least one stream")
stream, ok := streams[0].(map[string]interface{})
require.True(t, ok, "expected stream map")
stream["type"] = "api"
stream["params"] = map[string]interface{}{
"subdomain.example.com": "value2",
}
rawIn.Source, err = structpb.NewStruct(sourceMap)
require.NoError(t, err, "failed to rebuild proto source")
cfg, err := heartbeatCfg(&rawIn, &client.AgentInfo{ID: "abc7d0a8-ce04-4663-95da-ff6d537c268f", Version: "8.13.1"})
require.NoError(t, err, "heartbeatCfg returned error")
got, err := cfgToArrMap(cfg)
require.NoError(t, err, "failed to unpack cfg")
require.NotEmpty(t, got, "expected at least one monitor config")
params, ok := got[0]["params"].(map[string]interface{})
require.True(t, ok, "expected params map")
assert.Equal(t, "value2", params["subdomain.example.com"], "api monitor params dotted key should be preserved literally")
assert.NotContains(t, params, "subdomain", "api monitor params dotted key should not be expanded")
}
```
2. Run:
```bash
go test -count=1 -run TestReproAPIParamsDottedKeys ./x-pack/heartbeat/cmd
```
## Expected vs Actual
**Expected:** For `type: api`, dotted keys under `params` are preserved literally, so `params["subdomain.example.com"] == "value2"`.
**Actual:** Dotted key is expanded to nested structure; the literal key is missing.
Actual failing output:
```text
--- FAIL: TestReproAPIParamsDottedKeys (0.00s)
repro_api_params_test.go:43:
Error: Not equal:
expected: string("value2")
actual : ()
Messages: api monitor params dotted key should be preserved literally
repro_api_params_test.go:44:
Error: map[string]interface {}{"subdomain":map[string]interface {}{"example":map[string]interface {}{"com":"value2"}}} should not contain "subdomain"
Messages: api monitor params dotted key should not be expanded
FAIL
```
## Failing Test
(identical to the reproduction test above)
## Evidence
- `x-pack/heartbeat/cmd/root.go:79-80` only extracts params for `kind == "browser"`:
- `if kind, _ := stream["type"].(string); !ok || kind != "browser" { continue }`
- `x-pack/heartbeat/monitors/api/api.go:44` reuses browser source job:
- `sj, err := browser.NewSourceJob(cfg)`
- `x-pack/heartbeat/cmd/root_test.go:100-156` contains dotted-key preservation tests only for browser streams, not api streams.
---
[What is this?](https://ela.st/github-ai-tools) | [From workflow: Bug Hunter](https://github.com/elastic/beats/actions/runs/30539216718)
Give us feedback! React with 🚀 if perfect, 👍 if helpful, 👎 if not.
> - [x] expires on Aug 6, 2026, 11:51 AM UTC
Contributor guide
Research direction
Start with x-pack/heartbeat/cmd/root.go:79-80 and the config path used by x-pack/heartbeat/monitors/api/api.go:44. Compare the existing dotted-key tests in x-pack/heartbeat/cmd/root_test.go:100-156 with the reproduction test described in the issue, then run the focused Go test. Done means the API monitor preserves params["subdomain.example.com"] literally without nested expansion.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- go
- Domain
- api, testing
- Issue type
- Bug
- Difficulty
- 2/5
- Estimated time
- 1-3 hours
- Activity status
- Quiet
- Clarity
- Clearly specified
- Newbie friendliness
- 78/100