Skip to content

Commit 4debaef

Browse files
committed
refactor(notify): expose only custom data in templates
1 parent 6431ddc commit 4debaef

3 files changed

Lines changed: 48 additions & 95 deletions

File tree

README.md

Lines changed: 22 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -212,28 +212,27 @@ Templates use strict missing-key behavior. Missing fields fail during startup va
212212

213213
Templates receive the following data:
214214

215-
| Field | Type | Description |
216-
| ----------------- | ------------------- | ------------------------------------------------------------ |
217-
| `.IncidentID` | `string` | Stable ID for one overdue incident. |
218-
| `.NotificationID` | `string` | Stable ID for this concrete notification. |
219-
| `.CheckInName` | `string` | Configured check-in name. |
220-
| `.LastCheckIn` | `time.Time` | Last received check-in time. |
221-
| `.ExpectedBy` | `time.Time` | Time when the next check-in was expected. |
222-
| `.OverdueSince` | `time.Time` | Time when the check-in became overdue. |
223-
| `.AlertingAt` | `time.Time` | Time when alerting starts. |
224-
| `.Now` | `time.Time` | Time the notification event was created. |
225-
| `.Phase` | `string` | Monitor phase. |
226-
| `.Status` | `string` | Notification status: `alerting` or `resolved`. |
227-
| `.Resolved` | `bool` | Whether this is a resolved notification. |
228-
| `.Title` | `string` | Rendered notification title. |
229-
| `.Text` | `string` | Default plain text summary. |
230-
| `.Receiver` | `string` | Receiver name. |
231-
| `.Vars` | `map[string]any` | Public receiver variables. Custom data is also exposed here. |
232-
| `.CustomData` | `map[string]string` | Custom data configured with `custom-data`. |
233-
| `.App.Version` | `string` | Overdue version. |
234-
| `.App.SiteRoot` | `string` | Public base URL from `--public-url`. |
235-
| `.App.CheckInURL` | `string` | Public check-in URL when `--public-url` is configured. |
236-
| `.App.StatusURL` | `string` | Public status URL when `--public-url` is configured. |
215+
| Field | Type | Description |
216+
| ----------------- | ------------------- | ------------------------------------------------------ |
217+
| `.IncidentID` | `string` | Stable ID for one overdue incident. |
218+
| `.NotificationID` | `string` | Stable ID for this concrete notification. |
219+
| `.CheckInName` | `string` | Configured check-in name. |
220+
| `.LastCheckIn` | `time.Time` | Last received check-in time. |
221+
| `.ExpectedBy` | `time.Time` | Time when the next check-in was expected. |
222+
| `.OverdueSince` | `time.Time` | Time when the check-in became overdue. |
223+
| `.AlertingAt` | `time.Time` | Time when alerting starts. |
224+
| `.Now` | `time.Time` | Time the notification event was created. |
225+
| `.Phase` | `string` | Monitor phase. |
226+
| `.Status` | `string` | Notification status: `alerting` or `resolved`. |
227+
| `.Resolved` | `bool` | Whether this is a resolved notification. |
228+
| `.Title` | `string` | Rendered notification title. |
229+
| `.Text` | `string` | Default plain text summary. |
230+
| `.Receiver` | `string` | Receiver name. |
231+
| `.CustomData` | `map[string]string` | Custom data configured with `custom-data`. |
232+
| `.App.Version` | `string` | Overdue version. |
233+
| `.App.SiteRoot` | `string` | Public base URL from `--public-url`. |
234+
| `.App.CheckInURL` | `string` | Public check-in URL when `--public-url` is configured. |
235+
| `.App.StatusURL` | `string` | Public status URL when `--public-url` is configured. |
237236

238237
### Custom data
239238

@@ -243,13 +242,12 @@ Custom data is configured with `KEY=VALUE` values:
243242
-e OVERDUE__WEBHOOK_OPS_CUSTOM_DATA=channel=#alertmanager
244243
```
245244

246-
It is available in templates as both `.CustomData` and `.Vars`.
245+
It is available in templates as `.CustomData`.
247246

248247
Recommended access:
249248

250249
```gotemplate
251250
{{ index .CustomData "channel" }}
252-
{{ index .Vars "channel" }}
253251
```
254252

255253
Example with a default:

internal/notify/render.go

Lines changed: 6 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,6 @@ type Data struct {
3434
Text string
3535
App AppData
3636
Receiver string
37-
Vars map[string]any
3837
CustomData map[string]string
3938
}
4039

@@ -56,7 +55,6 @@ func NewData(event monitor.Event, receiver string, vars map[string]any, title st
5655
Text: text(event),
5756
App: appData(vars),
5857
Receiver: receiver,
59-
Vars: publicVars(vars),
6058
CustomData: customData(vars),
6159
}
6260
}
@@ -69,19 +67,12 @@ func text(event monitor.Event) string {
6967
return fmt.Sprintf(`Check-in %q is overdue:`, event.CheckInName)
7068
}
7169

72-
// varsFromConfig converts app and custom data into notifykit receiver variables.
70+
// varsFromConfig converts app data and custom data into notifykit receiver variables.
7371
func varsFromConfig(app AppData, custom map[string]string) map[string]any {
7472
vars := map[string]any{appVar: app}
75-
if len(custom) == 0 {
76-
return vars
73+
if len(custom) > 0 {
74+
vars[customDataVar] = cloneStringMap(custom)
7775
}
78-
79-
customCopy := make(map[string]string, len(custom))
80-
for key, value := range custom {
81-
customCopy[key] = value
82-
vars[key] = value
83-
}
84-
vars[customDataVar] = customCopy
8576
return vars
8677
}
8778

@@ -99,47 +90,14 @@ func customData(vars map[string]any) map[string]string {
9990
if len(vars) == 0 {
10091
return nil
10192
}
102-
if custom, ok := vars[customDataVar].(map[string]string); ok {
103-
return cloneStringMap(custom)
104-
}
105-
106-
custom := make(map[string]string, len(vars))
107-
for key, value := range vars {
108-
text, ok := value.(string)
109-
if ok {
110-
custom[key] = text
111-
}
112-
}
113-
if len(custom) == 0 {
114-
return nil
115-
}
116-
return custom
117-
}
118-
119-
// publicVars returns receiver variables intended for templates.
120-
func publicVars(vars map[string]any) map[string]any {
121-
if len(vars) == 0 {
122-
return nil
123-
}
124-
out := make(map[string]any, len(vars))
125-
for key, value := range vars {
126-
if key == appVar || key == customDataVar {
127-
continue
128-
}
129-
out[key] = value
130-
}
131-
if len(out) == 0 {
132-
return nil
133-
}
134-
return out
93+
custom, _ := vars[customDataVar].(map[string]string)
94+
return cloneStringMap(custom)
13595
}
13696

13797
// cloneStringMap returns a defensive copy of values.
13898
func cloneStringMap(values map[string]string) map[string]string {
13999
if len(values) == 0 {
140100
return nil
141101
}
142-
out := make(map[string]string, len(values))
143-
maps.Copy(out, values)
144-
return out
102+
return maps.Clone(values)
145103
}

internal/notify/render_test.go

Lines changed: 20 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,6 @@ func TestNewData(t *testing.T) {
3030
assert.Equal(t, "api", data.CheckInName)
3131
assert.Equal(t, "title", data.Title)
3232
assert.Equal(t, "ops", data.Receiver)
33-
assert.Equal(t, map[string]any{"channel": "alerts"}, data.Vars)
3433
assert.Equal(t, map[string]string{"channel": "alerts"}, data.CustomData)
3534
assert.Equal(t, "dev", data.App.Version)
3635
})
@@ -63,15 +62,24 @@ func TestVarsFromConfig(t *testing.T) {
6362
assert.Equal(t, map[string]any{appVar: AppData{Version: "dev"}}, varsFromConfig(AppData{Version: "dev"}, nil))
6463
})
6564

66-
t.Run("adds custom data as direct vars and CustomData", func(t *testing.T) {
65+
t.Run("adds custom data container", func(t *testing.T) {
6766
t.Parallel()
6867

6968
assert.Equal(t, map[string]any{
7069
appVar: AppData{Version: "dev"},
7170
customDataVar: map[string]string{"channel": "alerts"},
72-
"channel": "alerts",
7371
}, varsFromConfig(AppData{Version: "dev"}, map[string]string{"channel": "alerts"}))
7472
})
73+
74+
t.Run("copies custom data", func(t *testing.T) {
75+
t.Parallel()
76+
77+
custom := map[string]string{"channel": "alerts"}
78+
vars := varsFromConfig(AppData{Version: "dev"}, custom)
79+
custom["channel"] = "changed"
80+
81+
assert.Equal(t, map[string]string{"channel": "alerts"}, vars[customDataVar])
82+
})
7583
}
7684

7785
// TestAppData tests App extraction from receiver variables.
@@ -101,37 +109,26 @@ func TestCustomData(t *testing.T) {
101109
assert.Nil(t, customData(nil))
102110
})
103111

104-
t.Run("prefers configured CustomData map", func(t *testing.T) {
112+
t.Run("returns configured CustomData map", func(t *testing.T) {
105113
t.Parallel()
106114

107-
assert.Equal(t, map[string]string{"channel": "alerts"}, customData(map[string]any{customDataVar: map[string]string{"channel": "alerts"}, "ignored": 3}))
115+
assert.Equal(t, map[string]string{"channel": "alerts"}, customData(map[string]any{customDataVar: map[string]string{"channel": "alerts"}}))
108116
})
109117

110-
t.Run("falls back to string vars", func(t *testing.T) {
118+
t.Run("ignores direct string vars", func(t *testing.T) {
111119
t.Parallel()
112120

113-
assert.Equal(t, map[string]string{"channel": "alerts"}, customData(map[string]any{"channel": "alerts", "retries": 3}))
121+
assert.Nil(t, customData(map[string]any{"channel": "alerts"}))
114122
})
115-
}
116-
117-
// TestPublicVars tests public receiver variable extraction.
118-
func TestPublicVars(t *testing.T) {
119-
t.Parallel()
120123

121-
t.Run("returns nil without vars", func(t *testing.T) {
122-
t.Parallel()
123-
124-
assert.Nil(t, publicVars(nil))
125-
})
126-
127-
t.Run("excludes app and custom data container", func(t *testing.T) {
124+
t.Run("copies values", func(t *testing.T) {
128125
t.Parallel()
129126

130-
vars := map[string]any{appVar: AppData{Version: "dev"}, customDataVar: map[string]string{"channel": "alerts"}, "channel": "alerts"}
131-
got := publicVars(vars)
132-
vars["channel"] = "changed"
127+
values := map[string]string{"channel": "alerts"}
128+
got := customData(map[string]any{customDataVar: values})
129+
values["channel"] = "changed"
133130

134-
assert.Equal(t, map[string]any{"channel": "alerts"}, got)
131+
assert.Equal(t, map[string]string{"channel": "alerts"}, got)
135132
})
136133
}
137134

0 commit comments

Comments
 (0)