detectkit 0.13.1__tar.gz → 0.15.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (105) hide show
  1. {detectkit-0.13.1/detectkit.egg-info → detectkit-0.15.0}/PKG-INFO +2 -2
  2. {detectkit-0.13.1 → detectkit-0.15.0}/README.md +1 -1
  3. {detectkit-0.13.1 → detectkit-0.15.0}/detectkit/__init__.py +1 -1
  4. {detectkit-0.13.1 → detectkit-0.15.0}/detectkit/alerting/channels/base.py +15 -11
  5. {detectkit-0.13.1 → detectkit-0.15.0}/detectkit/alerting/channels/email.py +31 -5
  6. {detectkit-0.13.1 → detectkit-0.15.0}/detectkit/alerting/channels/telegram.py +6 -1
  7. {detectkit-0.13.1 → detectkit-0.15.0}/detectkit/alerting/channels/webhook.py +7 -2
  8. {detectkit-0.13.1 → detectkit-0.15.0}/detectkit/alerting/orchestrator/_base.py +6 -0
  9. {detectkit-0.13.1 → detectkit-0.15.0}/detectkit/alerting/orchestrator/_decision.py +2 -0
  10. {detectkit-0.13.1 → detectkit-0.15.0}/detectkit/alerting/orchestrator/_recovery.py +1 -0
  11. {detectkit-0.13.1 → detectkit-0.15.0}/detectkit/cli/assets/claude/CLAUDE.section.md +6 -1
  12. {detectkit-0.13.1 → detectkit-0.15.0}/detectkit/cli/assets/claude/rules/alerting.md +23 -1
  13. {detectkit-0.13.1 → detectkit-0.15.0}/detectkit/cli/assets/claude/rules/project.md +7 -3
  14. detectkit-0.15.0/detectkit/cli/assets/claude/skills/dtk-feedback/SKILL.md +155 -0
  15. {detectkit-0.13.1 → detectkit-0.15.0}/detectkit/cli/commands/init_claude.py +2 -1
  16. {detectkit-0.13.1 → detectkit-0.15.0}/detectkit/orchestration/task_manager/_alert_step.py +1 -0
  17. {detectkit-0.13.1 → detectkit-0.15.0/detectkit.egg-info}/PKG-INFO +2 -2
  18. {detectkit-0.13.1 → detectkit-0.15.0}/detectkit.egg-info/SOURCES.txt +1 -0
  19. {detectkit-0.13.1 → detectkit-0.15.0}/LICENSE +0 -0
  20. {detectkit-0.13.1 → detectkit-0.15.0}/MANIFEST.in +0 -0
  21. {detectkit-0.13.1 → detectkit-0.15.0}/detectkit/alerting/__init__.py +0 -0
  22. {detectkit-0.13.1 → detectkit-0.15.0}/detectkit/alerting/channels/__init__.py +0 -0
  23. {detectkit-0.13.1 → detectkit-0.15.0}/detectkit/alerting/channels/branding.py +0 -0
  24. {detectkit-0.13.1 → detectkit-0.15.0}/detectkit/alerting/channels/factory.py +0 -0
  25. {detectkit-0.13.1 → detectkit-0.15.0}/detectkit/alerting/channels/mattermost.py +0 -0
  26. {detectkit-0.13.1 → detectkit-0.15.0}/detectkit/alerting/channels/slack.py +0 -0
  27. {detectkit-0.13.1 → detectkit-0.15.0}/detectkit/alerting/orchestrator/__init__.py +0 -0
  28. {detectkit-0.13.1 → detectkit-0.15.0}/detectkit/alerting/orchestrator/_cooldown.py +0 -0
  29. {detectkit-0.13.1 → detectkit-0.15.0}/detectkit/alerting/orchestrator/_dispatch.py +0 -0
  30. {detectkit-0.13.1 → detectkit-0.15.0}/detectkit/alerting/orchestrator/_types.py +0 -0
  31. {detectkit-0.13.1 → detectkit-0.15.0}/detectkit/alerting/orchestrator/orchestrator.py +0 -0
  32. {detectkit-0.13.1 → detectkit-0.15.0}/detectkit/cli/__init__.py +0 -0
  33. {detectkit-0.13.1 → detectkit-0.15.0}/detectkit/cli/_output.py +0 -0
  34. {detectkit-0.13.1 → detectkit-0.15.0}/detectkit/cli/assets/claude/rules/cli.md +0 -0
  35. {detectkit-0.13.1 → detectkit-0.15.0}/detectkit/cli/assets/claude/rules/detectors.md +0 -0
  36. {detectkit-0.13.1 → detectkit-0.15.0}/detectkit/cli/assets/claude/rules/metrics.md +0 -0
  37. {detectkit-0.13.1 → detectkit-0.15.0}/detectkit/cli/assets/claude/rules/overview.md +0 -0
  38. {detectkit-0.13.1 → detectkit-0.15.0}/detectkit/cli/assets/claude/skills/dtk-new-metric/SKILL.md +0 -0
  39. {detectkit-0.13.1 → detectkit-0.15.0}/detectkit/cli/assets/claude/skills/dtk-setup-project/SKILL.md +0 -0
  40. {detectkit-0.13.1 → detectkit-0.15.0}/detectkit/cli/commands/__init__.py +0 -0
  41. {detectkit-0.13.1 → detectkit-0.15.0}/detectkit/cli/commands/clean.py +0 -0
  42. {detectkit-0.13.1 → detectkit-0.15.0}/detectkit/cli/commands/init.py +0 -0
  43. {detectkit-0.13.1 → detectkit-0.15.0}/detectkit/cli/commands/run.py +0 -0
  44. {detectkit-0.13.1 → detectkit-0.15.0}/detectkit/cli/commands/test_alert.py +0 -0
  45. {detectkit-0.13.1 → detectkit-0.15.0}/detectkit/cli/commands/unlock.py +0 -0
  46. {detectkit-0.13.1 → detectkit-0.15.0}/detectkit/cli/main.py +0 -0
  47. {detectkit-0.13.1 → detectkit-0.15.0}/detectkit/config/__init__.py +0 -0
  48. {detectkit-0.13.1 → detectkit-0.15.0}/detectkit/config/metric_config.py +0 -0
  49. {detectkit-0.13.1 → detectkit-0.15.0}/detectkit/config/profile.py +0 -0
  50. {detectkit-0.13.1 → detectkit-0.15.0}/detectkit/config/project_config.py +0 -0
  51. {detectkit-0.13.1 → detectkit-0.15.0}/detectkit/config/validator.py +0 -0
  52. {detectkit-0.13.1 → detectkit-0.15.0}/detectkit/core/__init__.py +0 -0
  53. {detectkit-0.13.1 → detectkit-0.15.0}/detectkit/core/interval.py +0 -0
  54. {detectkit-0.13.1 → detectkit-0.15.0}/detectkit/core/models.py +0 -0
  55. {detectkit-0.13.1 → detectkit-0.15.0}/detectkit/database/__init__.py +0 -0
  56. {detectkit-0.13.1 → detectkit-0.15.0}/detectkit/database/_sql_manager.py +0 -0
  57. {detectkit-0.13.1 → detectkit-0.15.0}/detectkit/database/clickhouse_manager.py +0 -0
  58. {detectkit-0.13.1 → detectkit-0.15.0}/detectkit/database/internal_tables/__init__.py +0 -0
  59. {detectkit-0.13.1 → detectkit-0.15.0}/detectkit/database/internal_tables/_alert_states.py +0 -0
  60. {detectkit-0.13.1 → detectkit-0.15.0}/detectkit/database/internal_tables/_base.py +0 -0
  61. {detectkit-0.13.1 → detectkit-0.15.0}/detectkit/database/internal_tables/_datapoints.py +0 -0
  62. {detectkit-0.13.1 → detectkit-0.15.0}/detectkit/database/internal_tables/_detections.py +0 -0
  63. {detectkit-0.13.1 → detectkit-0.15.0}/detectkit/database/internal_tables/_maintenance.py +0 -0
  64. {detectkit-0.13.1 → detectkit-0.15.0}/detectkit/database/internal_tables/_metrics.py +0 -0
  65. {detectkit-0.13.1 → detectkit-0.15.0}/detectkit/database/internal_tables/_schema.py +0 -0
  66. {detectkit-0.13.1 → detectkit-0.15.0}/detectkit/database/internal_tables/_tasks.py +0 -0
  67. {detectkit-0.13.1 → detectkit-0.15.0}/detectkit/database/internal_tables/manager.py +0 -0
  68. {detectkit-0.13.1 → detectkit-0.15.0}/detectkit/database/manager.py +0 -0
  69. {detectkit-0.13.1 → detectkit-0.15.0}/detectkit/database/mysql_manager.py +0 -0
  70. {detectkit-0.13.1 → detectkit-0.15.0}/detectkit/database/postgres_manager.py +0 -0
  71. {detectkit-0.13.1 → detectkit-0.15.0}/detectkit/database/tables.py +0 -0
  72. {detectkit-0.13.1 → detectkit-0.15.0}/detectkit/detectors/__init__.py +0 -0
  73. {detectkit-0.13.1 → detectkit-0.15.0}/detectkit/detectors/base.py +0 -0
  74. {detectkit-0.13.1 → detectkit-0.15.0}/detectkit/detectors/factory.py +0 -0
  75. {detectkit-0.13.1 → detectkit-0.15.0}/detectkit/detectors/seasonality.py +0 -0
  76. {detectkit-0.13.1 → detectkit-0.15.0}/detectkit/detectors/statistical/__init__.py +0 -0
  77. {detectkit-0.13.1 → detectkit-0.15.0}/detectkit/detectors/statistical/_windowed.py +0 -0
  78. {detectkit-0.13.1 → detectkit-0.15.0}/detectkit/detectors/statistical/iqr.py +0 -0
  79. {detectkit-0.13.1 → detectkit-0.15.0}/detectkit/detectors/statistical/mad.py +0 -0
  80. {detectkit-0.13.1 → detectkit-0.15.0}/detectkit/detectors/statistical/manual_bounds.py +0 -0
  81. {detectkit-0.13.1 → detectkit-0.15.0}/detectkit/detectors/statistical/zscore.py +0 -0
  82. {detectkit-0.13.1 → detectkit-0.15.0}/detectkit/loaders/__init__.py +0 -0
  83. {detectkit-0.13.1 → detectkit-0.15.0}/detectkit/loaders/metric_loader.py +0 -0
  84. {detectkit-0.13.1 → detectkit-0.15.0}/detectkit/loaders/query_template.py +0 -0
  85. {detectkit-0.13.1 → detectkit-0.15.0}/detectkit/orchestration/__init__.py +0 -0
  86. {detectkit-0.13.1 → detectkit-0.15.0}/detectkit/orchestration/error_dispatch.py +0 -0
  87. {detectkit-0.13.1 → detectkit-0.15.0}/detectkit/orchestration/task_manager/__init__.py +0 -0
  88. {detectkit-0.13.1 → detectkit-0.15.0}/detectkit/orchestration/task_manager/_base.py +0 -0
  89. {detectkit-0.13.1 → detectkit-0.15.0}/detectkit/orchestration/task_manager/_detect_step.py +0 -0
  90. {detectkit-0.13.1 → detectkit-0.15.0}/detectkit/orchestration/task_manager/_load_step.py +0 -0
  91. {detectkit-0.13.1 → detectkit-0.15.0}/detectkit/orchestration/task_manager/_types.py +0 -0
  92. {detectkit-0.13.1 → detectkit-0.15.0}/detectkit/orchestration/task_manager/manager.py +0 -0
  93. {detectkit-0.13.1 → detectkit-0.15.0}/detectkit/utils/__init__.py +0 -0
  94. {detectkit-0.13.1 → detectkit-0.15.0}/detectkit/utils/datetime_utils.py +0 -0
  95. {detectkit-0.13.1 → detectkit-0.15.0}/detectkit/utils/env_interpolation.py +0 -0
  96. {detectkit-0.13.1 → detectkit-0.15.0}/detectkit/utils/json_utils.py +0 -0
  97. {detectkit-0.13.1 → detectkit-0.15.0}/detectkit/utils/stats.py +0 -0
  98. {detectkit-0.13.1 → detectkit-0.15.0}/detectkit.egg-info/dependency_links.txt +0 -0
  99. {detectkit-0.13.1 → detectkit-0.15.0}/detectkit.egg-info/entry_points.txt +0 -0
  100. {detectkit-0.13.1 → detectkit-0.15.0}/detectkit.egg-info/requires.txt +0 -0
  101. {detectkit-0.13.1 → detectkit-0.15.0}/detectkit.egg-info/top_level.txt +0 -0
  102. {detectkit-0.13.1 → detectkit-0.15.0}/pyproject.toml +0 -0
  103. {detectkit-0.13.1 → detectkit-0.15.0}/requirements.txt +0 -0
  104. {detectkit-0.13.1 → detectkit-0.15.0}/setup.cfg +0 -0
  105. {detectkit-0.13.1 → detectkit-0.15.0}/setup.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: detectkit
3
- Version: 0.13.1
3
+ Version: 0.15.0
4
4
  Summary: Metric monitoring with automatic anomaly detection
5
5
  Author: detectkit team
6
6
  License: MIT
@@ -87,7 +87,7 @@ Dynamic: license-file
87
87
  - **Database agnostic** — ClickHouse, PostgreSQL, MySQL
88
88
  - **Idempotent** — resume from interruptions, no duplicate processing
89
89
  - **CLI** — `dtk init`, `dtk run --select`, `dtk unlock`, `dtk clean`, tag-based selectors
90
- - **AI-native onboarding** — `dtk init-claude` sets up Claude Code context (CLAUDE.md + rules + a metric-scaffolding skill) so an assistant can help you build metrics out of the box
90
+ - **AI-native onboarding** — `dtk init-claude` sets up Claude Code context (CLAUDE.md + rules + three skills) so an assistant can scaffold metrics, configure databases, and file feedback upstream
91
91
 
92
92
  ## Installation
93
93
 
@@ -19,7 +19,7 @@
19
19
  - **Database agnostic** — ClickHouse, PostgreSQL, MySQL
20
20
  - **Idempotent** — resume from interruptions, no duplicate processing
21
21
  - **CLI** — `dtk init`, `dtk run --select`, `dtk unlock`, `dtk clean`, tag-based selectors
22
- - **AI-native onboarding** — `dtk init-claude` sets up Claude Code context (CLAUDE.md + rules + a metric-scaffolding skill) so an assistant can help you build metrics out of the box
22
+ - **AI-native onboarding** — `dtk init-claude` sets up Claude Code context (CLAUDE.md + rules + three skills) so an assistant can scaffold metrics, configure databases, and file feedback upstream
23
23
 
24
24
  ## Installation
25
25
 
@@ -4,7 +4,7 @@ detectk - Anomaly Detection for Time-Series Metrics
4
4
  A Python library for data analysts and engineers to monitor metrics with automatic anomaly detection.
5
5
  """
6
6
 
7
- __version__ = "0.13.1"
7
+ __version__ = "0.15.0"
8
8
 
9
9
  from detectkit.core.interval import Interval
10
10
  from detectkit.core.models import ColumnDefinition, TableModel
@@ -32,10 +32,14 @@ class AlertData:
32
32
  consecutive_count: Number of consecutive anomalies
33
33
  is_recovery: True for recovery notifications
34
34
  is_no_data: True for missing-data alerts (no_data_alert)
35
- project_name: Optional ``detectkit_project.yml`` name. Surfaces
36
- as ``{project_name}`` in templates and as a ``[name] `` prefix
37
- in the default error title. Lets multiple projects share the
38
- same alert channel without ambiguity.
35
+ project_name: Optional ``detectkit_project.yml`` name. Surfaces as
36
+ ``{project_name}`` / ``{project_name_prefix}`` in templates and, by
37
+ default, as a ``[name] `` prefix on every alert title/headline
38
+ (anomaly, recovery, no-data, error) plus a brand-paired footer
39
+ ("detectkit · name" on webhook/email). The detectkit pipeline stamps
40
+ it from the project config; direct-API callers leave it ``None`` and
41
+ render unchanged. Lets multiple projects share one alert channel —
42
+ keeping the default brand bot name + avatar — without ambiguity.
39
43
 
40
44
  Alert-rule fields (``min_detectors``, ``direction_policy``,
41
45
  ``consecutive_required``, ``detector_count``) describe *why the alert
@@ -450,7 +454,7 @@ class BaseAlertChannel(ABC):
450
454
  Default template string
451
455
  """
452
456
  return (
453
- "🔴 Alert: {metric_name}\n"
457
+ "🔴 {project_name_prefix}Alert: {metric_name}\n"
454
458
  "{description_line}"
455
459
  "Quorum {detector_count}/{min_detectors} · "
456
460
  "direction {direction} (policy {direction_policy}) · "
@@ -476,7 +480,7 @@ class BaseAlertChannel(ABC):
476
480
  Default recovery template string
477
481
  """
478
482
  return (
479
- "🟢 Alert cleared: {metric_name}\n"
483
+ "🟢 {project_name_prefix}Alert cleared: {metric_name}\n"
480
484
  "{description_line}"
481
485
  "The alert condition no longer holds — "
482
486
  "the metric is back within expected bounds.\n"
@@ -500,7 +504,7 @@ class BaseAlertChannel(ABC):
500
504
  Returns:
501
505
  Default title template string
502
506
  """
503
- return "🔴 Alert: {metric_name}"
507
+ return "🔴 {project_name_prefix}Alert: {metric_name}"
504
508
 
505
509
  def get_default_recovery_title_template(self) -> str:
506
510
  """
@@ -509,7 +513,7 @@ class BaseAlertChannel(ABC):
509
513
  Returns:
510
514
  Default recovery title template string
511
515
  """
512
- return "🟢 Alert cleared: {metric_name}"
516
+ return "🟢 {project_name_prefix}Alert cleared: {metric_name}"
513
517
 
514
518
  def get_default_no_data_template(self) -> str:
515
519
  """
@@ -519,7 +523,7 @@ class BaseAlertChannel(ABC):
519
523
  has no datapoint (no row OR row with NULL/NaN value).
520
524
  """
521
525
  return (
522
- "🟡 No data for metric: {metric_name}\n"
526
+ "🟡 {project_name_prefix}No data for metric: {metric_name}\n"
523
527
  "{description_line}"
524
528
  "Time: {timestamp}\n"
525
529
  "Status: query returned no datapoint for the latest interval\n"
@@ -529,12 +533,12 @@ class BaseAlertChannel(ABC):
529
533
 
530
534
  def get_default_no_data_title_template(self) -> str:
531
535
  """Get default title template for no-data alerts."""
532
- return "🟡 No data: {metric_name}"
536
+ return "🟡 {project_name_prefix}No data: {metric_name}"
533
537
 
534
538
  def get_default_error_template(self) -> str:
535
539
  """Default body template for project-level error alerts."""
536
540
  return (
537
- "🔵 Pipeline failed for metric: {metric_name}\n"
541
+ "🔵 {project_name_prefix}Pipeline failed for metric: {metric_name}\n"
538
542
  "{description_line}"
539
543
  "Time: {timestamp}\n"
540
544
  "Error: {error_type}: {error_message}\n"
@@ -74,7 +74,7 @@ class EmailChannel(BaseAlertChannel):
74
74
  smtp_username: str | None = None,
75
75
  smtp_password: str | None = None,
76
76
  use_tls: bool = True,
77
- subject_template: str = "🔴 Alert: {metric_name}",
77
+ subject_template: str = "🔴 {project_name_prefix}Alert: {metric_name}",
78
78
  from_name: str = BRAND_USERNAME,
79
79
  template: str | None = None,
80
80
  **kwargs,
@@ -144,10 +144,18 @@ class EmailChannel(BaseAlertChannel):
144
144
  # of a bot display name. formataddr quotes the name when required.
145
145
  msg["From"] = formataddr((self.from_name, self.from_email))
146
146
  msg["To"] = ", ".join(self.to_emails)
147
- # Strip CR/LF from the metric name before it reaches the Subject header
148
- # so a metric name can never inject extra email headers.
147
+ # Strip CR/LF from the metric *and* project name before they reach the
148
+ # Subject header so neither can inject extra email headers. The project
149
+ # prefix ("[my_project] ") makes the inbox row distinguishable when
150
+ # several projects email the same address.
149
151
  subject_metric = alert_data.metric_name.replace("\r", " ").replace("\n", " ")
150
- msg["Subject"] = self.subject_template.format(metric_name=subject_metric)
152
+ project_clean = (alert_data.project_name or "").replace("\r", " ").replace("\n", " ")
153
+ project_prefix = f"[{project_clean}] " if project_clean else ""
154
+ msg["Subject"] = self.subject_template.format(
155
+ metric_name=subject_metric,
156
+ project_name_prefix=project_prefix,
157
+ project_name=project_clean,
158
+ )
151
159
 
152
160
  # Attach both parts. In multipart/alternative the LAST part is the
153
161
  # preferred one, so HTML (the branded card) is shown when supported and
@@ -247,6 +255,8 @@ class EmailChannel(BaseAlertChannel):
247
255
  f'font-size:4px;background-color:{accent};">&nbsp;</td></tr>'
248
256
  # header: logo + wordmark + status pill
249
257
  f"{self._header_html(accent, pill)}"
258
+ # project eyebrow (small label above the metric, only when set)
259
+ f"{self._eyebrow_html(ctx['project_name'])}"
250
260
  # title
251
261
  f'<tr><td style="padding:6px 24px 2px 24px;font-family:{_SANS};font-size:22px;'
252
262
  f'font-weight:bold;color:{_INK};mso-line-height-rule:exactly;line-height:28px;">'
@@ -258,6 +268,17 @@ class EmailChannel(BaseAlertChannel):
258
268
  "</table></td></tr></table></body></html>"
259
269
  )
260
270
 
271
+ def _eyebrow_html(self, project_name: str) -> str:
272
+ """Small uppercase project label above the metric title (empty if unset)."""
273
+ if not project_name:
274
+ return ""
275
+ return (
276
+ f'<tr><td style="padding:10px 24px 0 24px;font-family:{_SANS};font-size:11px;'
277
+ f"font-weight:bold;letter-spacing:0.5px;color:{_FAINT};text-transform:uppercase;"
278
+ f'mso-line-height-rule:exactly;line-height:14px;">'
279
+ f"{html.escape(project_name)}</td></tr>"
280
+ )
281
+
261
282
  def _header_html(self, accent: str, pill: str) -> str:
262
283
  """Logo + wordmark (real text) + colored status pill row."""
263
284
  return (
@@ -432,11 +453,16 @@ class EmailChannel(BaseAlertChannel):
432
453
  def _footer_html(self, alert_data: AlertData) -> str:
433
454
  cc = self.format_mentions(alert_data.mentions)
434
455
  cc_html = f" &middot; {html.escape(cc)}" if cc else ""
456
+ # Pair the brand with the project name ("Sent by detectkit · my_project")
457
+ # so the source project is clear even past the subject/eyebrow.
458
+ project_html = (
459
+ f" &middot; {html.escape(alert_data.project_name)}" if alert_data.project_name else ""
460
+ )
435
461
  return (
436
462
  f'<tr><td bgcolor="{_SURFACE}" style="background-color:{_SURFACE};'
437
463
  f"border-top:1px solid {_BORDER};padding:14px 24px;font-family:{_SANS};"
438
464
  f'font-size:12px;color:{_FAINT};mso-line-height-rule:exactly;line-height:16px;">'
439
- f"Sent by detectkit{cc_html}</td></tr>"
465
+ f"Sent by detectkit{project_html}{cc_html}</td></tr>"
440
466
  )
441
467
 
442
468
  def format_mentions(self, mentions: list[str]) -> str:
@@ -148,7 +148,12 @@ class TelegramChannel(BaseAlertChannel):
148
148
  return html.escape(str(value))
149
149
 
150
150
  metric = esc(ctx["metric_name"])
151
- lines: list[str] = [f"{dot} <b>{word} · {metric}</b>"]
151
+ # Lead the headline with the project name (when set) so multiple
152
+ # projects sharing one chat stay distinguishable — Telegram has no
153
+ # footer/avatar override, so this prefix is the only project cue.
154
+ proj = ctx["project_name"]
155
+ head_prefix = f"[{esc(proj)}] " if proj else ""
156
+ lines: list[str] = [f"{dot} <b>{head_prefix}{word} · {metric}</b>"]
152
157
 
153
158
  if ctx["description"]:
154
159
  lines.append(f"<i>{esc(self._cap(ctx['description'], _DESC_CAP))}</i>")
@@ -189,8 +189,13 @@ class WebhookChannel(BaseAlertChannel):
189
189
  attachment["title_link"] = alert_data.dashboard_url
190
190
 
191
191
  # Brand the attachment footer (reliable on Slack even when top-level
192
- # username/icon are locked to the app install).
193
- attachment["footer"] = self.username or BRAND_USERNAME
192
+ # username/icon are locked to the app install). Pair the brand name with
193
+ # the project name when set ("detectkit · my_project") so two projects
194
+ # posting to the same channel stay distinguishable even past the title.
195
+ footer = self.username or BRAND_USERNAME
196
+ if alert_data.project_name:
197
+ footer = f"{footer} · {alert_data.project_name}"
198
+ attachment["footer"] = footer
194
199
  if self.icon_url:
195
200
  attachment["footer_icon"] = self.icon_url
196
201
  # Slack-only sugar: a real timestamp under the footer. Mattermost
@@ -25,6 +25,7 @@ class _OrchestratorBase:
25
25
  mentions: list[str] | None = None,
26
26
  dashboard_url: str | None = None,
27
27
  links: dict[str, str] | None = None,
28
+ project_name: str | None = None,
28
29
  ):
29
30
  self.metric_name = metric_name
30
31
  self.interval = interval
@@ -37,6 +38,11 @@ class _OrchestratorBase:
37
38
  self.mentions = mentions or []
38
39
  self.dashboard_url = dashboard_url
39
40
  self.links = links or {}
41
+ # Optional project name (``detectkit_project.yml`` ``name``). Stamped
42
+ # onto every AlertData so channels can label which project an alert
43
+ # came from — keeps multiple projects sharing one channel distinct
44
+ # while the bot keeps the default brand name + avatar.
45
+ self.project_name = project_name
40
46
 
41
47
  @staticmethod
42
48
  def _group_by_timestamp(
@@ -241,6 +241,7 @@ class _DecisionMixin(_OrchestratorBase):
241
241
  mentions=self.mentions,
242
242
  dashboard_url=self.dashboard_url,
243
243
  links=self.links,
244
+ project_name=self.project_name,
244
245
  # Alert rule the message foregrounds: configured thresholds plus
245
246
  # the observed quorum size that satisfied them.
246
247
  min_detectors=self.conditions.min_detectors,
@@ -300,6 +301,7 @@ class _DecisionMixin(_OrchestratorBase):
300
301
  mentions=self.mentions,
301
302
  dashboard_url=self.dashboard_url,
302
303
  links=self.links,
304
+ project_name=self.project_name,
303
305
  )
304
306
 
305
307
  def get_last_complete_point(self, now: datetime | None = None) -> datetime:
@@ -178,6 +178,7 @@ class _RecoveryMixin(_OrchestratorBase):
178
178
  mentions=self.mentions,
179
179
  dashboard_url=self.dashboard_url,
180
180
  links=self.links,
181
+ project_name=self.project_name,
181
182
  # Echo the rule that had fired so the recovery message names the
182
183
  # same alert condition that just cleared.
183
184
  min_detectors=self.conditions.min_detectors,
@@ -26,7 +26,7 @@ version — **read the relevant one on demand** instead of guessing:
26
26
  | Choosing/tuning detectors, preprocessing, trends, seasonality | `.claude/rules/detectkit/detectors.md` |
27
27
  | Alert rules (quorum/direction/consecutive), cooldown, recovery, templates | `.claude/rules/detectkit/alerting.md` |
28
28
 
29
- ### Set up & scaffold (skills)
29
+ ### Skills
30
30
 
31
31
  - **First-time setup** — use the **`dtk-setup-project`** skill to configure the
32
32
  database connection in `profiles.yml` (the `dtk init` placeholder ships example
@@ -34,6 +34,11 @@ version — **read the relevant one on demand** instead of guessing:
34
34
  channel.
35
35
  - **A new metric** — use the **`dtk-new-metric`** skill; it walks the config out
36
36
  to a YAML file that validates and is ready to run.
37
+ - **Hit a detectkit bug, or have feedback** — once you've ruled out a local
38
+ config fix (see the gotchas below), use the **`dtk-feedback`** skill to file a
39
+ redacted bug report, feature request, or comment as a GitHub issue on the
40
+ upstream repo. It auto-collects the diagnostic context, strips every secret,
41
+ and never submits without showing you the exact text first.
37
42
 
38
43
  ### Gotchas that bite (keep these in mind)
39
44
 
@@ -158,7 +158,9 @@ With no custom `template`, each channel renders a structured, branded message
158
158
  shared value computation lives in one place (`BaseAlertChannel.build_context`),
159
159
  so templates and native rendering stay consistent. Every alert title/headline
160
160
  leads with a colored **status circle** — 🔴 anomaly, 🟢 recovery, 🟡 no-data,
161
- 🔵 pipeline error — so the status reads from color alone.
161
+ 🔵 pipeline error — so the status reads from color alone. It also leads with the
162
+ **project name** as a `[name] ` prefix (from `detectkit_project.yml`) — see
163
+ [Project label](#project-label-multi-project-channels) below.
162
164
 
163
165
  - **Slack / Mattermost / generic webhook** — one message *attachment* with a
164
166
  status-colored accent bar, a clickable title (the metric; links to
@@ -181,6 +183,25 @@ leads with a colored **status circle** — 🔴 anomaly, 🟢 recovery, 🟡 no-
181
183
  table, a monospace params box, an optional "Open dashboard" button, and a
182
184
  footer. The plain-text body remains the multipart fallback.
183
185
 
186
+ ## Project label (multi-project channels)
187
+
188
+ The bot keeps the **detectkit brand** name + avatar by default (so users rarely
189
+ override them). To still tell apart two projects posting to the **same** channel,
190
+ detectkit stamps the project name (`detectkit_project.yml` → `name`) onto every
191
+ alert and shows it by default — no config needed:
192
+
193
+ - **Title / headline / subject** lead with a `[name] ` prefix on every kind
194
+ (anomaly, recovery, no-data, error): `🔴 [payments] Alert: api_error_rate`.
195
+ - **Webhook (Slack/Mattermost)** also pairs it in the footer: `detectkit · payments`.
196
+ - **Telegram** carries it in the bold headline (no footer/avatar to override).
197
+ - **Email** prefixes the subject, shows a small project eyebrow above the metric,
198
+ and pairs it in the footer (`Sent by detectkit · payments`).
199
+
200
+ It is exposed to custom templates as `{project_name}` and `{project_name_prefix}`
201
+ (`"[name] "` when set, else `""`). Direct library/API callers that don't set it
202
+ render unchanged. The `name` is informational only (it does not key any `_dtk_*`
203
+ table), so renaming it is safe — spaces are allowed for a prettier label.
204
+
184
205
  ## Multiple alert configs per metric
185
206
 
186
207
  `alerting:` may be a **list** of independent blocks, each with its own channels,
@@ -210,6 +231,7 @@ referenced by path). Key variables:
210
231
  | Variable | Meaning |
211
232
  |---|---|
212
233
  | `{metric_name}`, `{description}` / `{description_line}` | identity |
234
+ | `{project_name}` / `{project_name_prefix}` | project label (`"[name] "` prefix, or `""`) |
213
235
  | `{timestamp}`, `{timezone}` | when (display tz via `alerting.timezone`, default UTC) |
214
236
  | `{value}` / `{value_display}` | metric value (`value_display` is NaN-safe) |
215
237
  | `{confidence_lower}` / `{confidence_upper}` / `{confidence_interval}` | bounds |
@@ -9,7 +9,9 @@ errors (not empty strings).
9
9
  ## `detectkit_project.yml`
10
10
 
11
11
  ```yaml
12
- name: my_monitoring # required — project identifier (logs, error-alert titles)
12
+ name: my_monitoring # required — project identifier; also labels every
13
+ # alert ("[my_monitoring] Alert: …") so multiple
14
+ # projects on one channel stay distinct (alerting.md)
13
15
  version: "1.0" # optional (default "1.0")
14
16
  default_profile: prod # profile name from profiles.yml
15
17
 
@@ -57,8 +59,10 @@ error_alerting:
57
59
  via cron cadence). Channel send failures are swallowed so a flaky webhook
58
60
  can't crash the run.
59
61
  - Extra template variables: `{error_type}`, `{error_message}`, `{status}`
60
- (always `"ERROR"`), `{project_name}`, `{project_name_prefix}` (=
61
- `"[<name>] "` when `name` set keeps multi-project channels distinguishable).
62
+ (always `"ERROR"`). `{project_name}` / `{project_name_prefix}` (=
63
+ `"[<name>] "` when `name` set) are available here **and in every other alert
64
+ template** — and by default lead the title/headline on all channels, keeping
65
+ multi-project channels distinguishable (see `alerting.md` → Project label).
62
66
 
63
67
  ## `profiles.yml`
64
68
 
@@ -0,0 +1,155 @@
1
+ ---
2
+ name: dtk-feedback
3
+ description: >-
4
+ Report a detectkit bug, request a feature, or send feedback to the maintainers
5
+ as a GitHub issue on the upstream repo (alexeiveselov92/detectkit). Use when a
6
+ dtk command errors or behaves unexpectedly and it looks like a detectkit defect
7
+ (not the user's config), when the user wishes detectkit did something it
8
+ doesn't, or when they ask to report/file an issue, send feedback, or contact
9
+ the maintainers. Auto-collects diagnostic context, redacts all secrets, and
10
+ never submits without explicit confirmation.
11
+ ---
12
+
13
+ # Send feedback to the detectkit maintainers
14
+
15
+ Turn a problem, a wish, or a comment into a clear GitHub issue on the upstream
16
+ repo **`alexeiveselov92/detectkit`** — carrying the diagnostic context
17
+ maintainers need and **leaking no secrets**. You are filing a *public* issue on
18
+ the user's behalf: never submit anything without showing the exact text and
19
+ getting an explicit "yes".
20
+
21
+ This skill is the procedure; the reference for ruling out config problems lives
22
+ under `.claude/rules/detectkit/`.
23
+
24
+ ## Step 0 — Decide whether this belongs upstream
25
+
26
+ - **Bug** — a `dtk` command errors or produces clearly wrong output, **and**
27
+ you've ruled out a local config/usage mistake (Step 1).
28
+ - **Feature request** — the user needs detectkit to do something it doesn't.
29
+ - **Feedback / question** — a comment, confusion, or rough edge worth surfacing.
30
+
31
+ Do **not** open an issue for the user's own SQL, data, database, or
32
+ configuration mistakes — those are fixed locally with the rules files, not
33
+ reported as detectkit bugs. Only genuine detectkit behavior goes upstream.
34
+
35
+ ## Step 1 — Rule out config first (for "it doesn't work")
36
+
37
+ Before calling something a bug, confirm it isn't a configuration or usage
38
+ problem. Read the matching `.claude/rules/detectkit/` file and check the usual
39
+ gotchas:
40
+
41
+ - the loading query filters on `{{ dtk_start_time }}` **and** `{{ dtk_end_time }}`;
42
+ - metric `name` is unique across the project;
43
+ - `profiles.yml` has the right location fields (`internal_database`/`data_database`,
44
+ or Postgres schemas) and a `default_profile` that exists;
45
+ - `alert_cooldown` is set; channels referenced by metrics exist.
46
+
47
+ If a local fix exists, **do that and stop** — don't file an issue. Escalate only
48
+ what is genuinely a detectkit defect.
49
+
50
+ ## Step 2 — Gather context (do this for the user, don't make them dig)
51
+
52
+ Collect automatically:
53
+
54
+ - `dtk --version` — the detectkit version (**always** include it).
55
+ - Python version (`python3 --version`) and OS.
56
+ - The database backend **type only** (`clickhouse` / `postgres` / `mysql`) from
57
+ `profiles.yml`'s `type:` — never the host, port, or credentials.
58
+
59
+ For a **bug**, also gather:
60
+
61
+ - the exact command that was run;
62
+ - the full error message / traceback;
63
+ - what the user expected vs. what happened;
64
+ - a **minimal reproduction** — the failing metric YAML and/or SQL, with every
65
+ secret stripped (Step 3).
66
+
67
+ ## Step 3 — Redact secrets (mandatory — before anything is shown or sent)
68
+
69
+ This is a public issue. Strip from every snippet you include:
70
+
71
+ - passwords, tokens, `bot_token`, `smtp_password`, API keys;
72
+ - `webhook_url`s, `chat_id`s, email addresses;
73
+ - hostnames, IPs, ports, and real database / schema / table names;
74
+ - the resolved value of anything inside `{{ env_var('…') }}` or `${…}` — keep the
75
+ `env_var(...)` shape, drop the value.
76
+
77
+ Replace with placeholders like `<redacted>`, `<your-host>`, `<your_table>` while
78
+ keeping the structure intact so the bug is still reproducible. **If you're not
79
+ sure whether something is sensitive, redact it.**
80
+
81
+ ## Step 4 — Search for duplicates
82
+
83
+ ```bash
84
+ gh issue list --repo alexeiveselov92/detectkit --search "<keywords>" --state all
85
+ ```
86
+
87
+ If a matching issue already exists, offer to **add a comment** or a 👍 reaction
88
+ instead of opening a new one:
89
+
90
+ ```bash
91
+ gh issue comment <number> --repo alexeiveselov92/detectkit --body "<note>"
92
+ ```
93
+
94
+ This gives maintainers signal on frequency instead of a pile of duplicates.
95
+
96
+ ## Step 5 — Draft the issue
97
+
98
+ Write a specific title (the actual symptom in a few words, not "bug"). Pick the
99
+ body template by type:
100
+
101
+ - **Bug** — `Summary` / `Environment` (detectkit version, Python, OS, backend) /
102
+ `Steps to reproduce` / `Expected` / `Actual` (with the traceback in a fenced
103
+ code block) / `Minimal config` (redacted).
104
+ - **Feature request** — `Problem` (the underlying need, not just the proposed
105
+ fix) / `Proposed behavior` / `Alternatives considered`.
106
+ - **Feedback** — free-form, concrete and respectful.
107
+
108
+ End the body with an attribution marker so maintainers can see it came through
109
+ the assistant funnel and on which version:
110
+
111
+ ```
112
+ _Filed via the dtk init-claude assistant (detectkit <version>)._
113
+ ```
114
+
115
+ ## Step 6 — Preview and confirm (no silent submits)
116
+
117
+ Show the user the **full** title and body exactly as they will be posted, and
118
+ the target repo. Ask them to confirm: secrets redacted? right repo? Proceed to
119
+ Step 7 only on an explicit "yes". If they want edits, revise and re-preview.
120
+
121
+ ## Step 7 — Submit
122
+
123
+ **Preferred — `gh` CLI** (check it's installed and authenticated first with
124
+ `gh auth status`). Write the body to a temp file to avoid shell-escaping the
125
+ markdown/traceback:
126
+
127
+ ```bash
128
+ gh issue create --repo alexeiveselov92/detectkit \
129
+ --title "<title>" --body-file <tmpfile>
130
+ ```
131
+
132
+ Tag the issue so maintainers can triage assistant-filed reports: add the type
133
+ label (`--label bug` / `--label enhancement`) **and** `--label "via:assistant"`.
134
+ Labels must already exist on the repo, so if `gh` reports an unknown label,
135
+ **retry without the failing label(s)** — never fail the report over a missing
136
+ label. (Attribution also lives in the Step 5 body marker, so maintainers can
137
+ filter with `in:body "Filed via the dtk init-claude assistant"` even before the
138
+ `via:assistant` label exists.) Return the issue URL to the user.
139
+
140
+ **Fallback — no `gh`, or not authenticated.** Build a prefilled "new issue" URL
141
+ and hand it to the user to open and submit in their browser:
142
+
143
+ ```
144
+ https://github.com/alexeiveselov92/detectkit/issues/new?title=<url-encoded>&body=<url-encoded>
145
+ ```
146
+
147
+ URL-encode both fields. If the body is too long for a URL, give the user the
148
+ title plus the body text to paste, and the plain link
149
+ `https://github.com/alexeiveselov92/detectkit/issues/new`.
150
+
151
+ ## Step 8 — Close the loop
152
+
153
+ Give the user the issue URL (or the prefilled link), mention they can subscribe
154
+ for updates, and — if you also worked around the bug locally — restate that
155
+ local fix so they're unblocked right now, not just waiting on the issue.
@@ -11,7 +11,8 @@ It writes three things into the target directory:
11
11
  injected/refreshed between HTML-comment markers (existing content is kept).
12
12
  - ``.claude/rules/detectkit/`` — the reference docs the assistant reads on
13
13
  demand (overview, cli, project, metrics, detectors, alerting).
14
- - ``.claude/skills/`` — user-facing skills (``dtk-new-metric``).
14
+ - ``.claude/skills/`` — user-facing skills (``dtk-setup-project``,
15
+ ``dtk-new-metric``, ``dtk-feedback``).
15
16
 
16
17
  The source of truth for all of the above lives in ``detectkit/cli/assets/claude``
17
18
  and ships with the package, so re-running this command after upgrading detectkit
@@ -64,6 +64,7 @@ class _AlertStepMixin(_TaskManagerBase):
64
64
  mentions=alerting_config.mentions,
65
65
  dashboard_url=alerting_config.dashboard_url,
66
66
  links=alerting_config.links,
67
+ project_name=getattr(self.project_config, "name", None),
67
68
  )
68
69
 
69
70
  last_point = orchestrator.get_last_complete_point()
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: detectkit
3
- Version: 0.13.1
3
+ Version: 0.15.0
4
4
  Summary: Metric monitoring with automatic anomaly detection
5
5
  Author: detectkit team
6
6
  License: MIT
@@ -87,7 +87,7 @@ Dynamic: license-file
87
87
  - **Database agnostic** — ClickHouse, PostgreSQL, MySQL
88
88
  - **Idempotent** — resume from interruptions, no duplicate processing
89
89
  - **CLI** — `dtk init`, `dtk run --select`, `dtk unlock`, `dtk clean`, tag-based selectors
90
- - **AI-native onboarding** — `dtk init-claude` sets up Claude Code context (CLAUDE.md + rules + a metric-scaffolding skill) so an assistant can help you build metrics out of the box
90
+ - **AI-native onboarding** — `dtk init-claude` sets up Claude Code context (CLAUDE.md + rules + three skills) so an assistant can scaffold metrics, configure databases, and file feedback upstream
91
91
 
92
92
  ## Installation
93
93
 
@@ -39,6 +39,7 @@ detectkit/cli/assets/claude/rules/detectors.md
39
39
  detectkit/cli/assets/claude/rules/metrics.md
40
40
  detectkit/cli/assets/claude/rules/overview.md
41
41
  detectkit/cli/assets/claude/rules/project.md
42
+ detectkit/cli/assets/claude/skills/dtk-feedback/SKILL.md
42
43
  detectkit/cli/assets/claude/skills/dtk-new-metric/SKILL.md
43
44
  detectkit/cli/assets/claude/skills/dtk-setup-project/SKILL.md
44
45
  detectkit/cli/commands/__init__.py
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes