Run this prompt in Claude Code (or Codex / Cursor with the skill installed). The{" "}
maple-onboard skill walks every service
diff --git a/apps/web/src/components/ingest/use-ingest-connection.ts b/apps/web/src/components/ingest/use-ingest-connection.ts
index d9cdf07e..e19fe407 100644
--- a/apps/web/src/components/ingest/use-ingest-connection.ts
+++ b/apps/web/src/components/ingest/use-ingest-connection.ts
@@ -14,6 +14,8 @@ export interface IngestConnection {
serviceCount: number
/** First real service name, if any — handy for "we're seeing X" copy. */
firstRealService?: string
+ /** Spans per minute across real services, averaged over the 1h window. */
+ spansPerMinute: number
/** The org's public ingest key (empty string until loaded / when denied). */
apiKey: string
/** Force an immediate re-poll of the service overview. */
@@ -50,11 +52,13 @@ export function useIngestConnection({ poll = true }: UseIngestConnectionOptions
)
const firstRealService =
typeof realServices[0]?.serviceName === "string" ? (realServices[0].serviceName as string) : undefined
+ const spansPerMinute = realServices.reduce((sum, s) => sum + (s.spanCount ?? 0), 0) / 60
return {
status: realServices.length > 0 ? "connected" : "waiting",
serviceCount: realServices.length,
firstRealService,
+ spansPerMinute,
apiKey,
refresh,
}
diff --git a/apps/web/src/components/settings/attribute-mappings-section.tsx b/apps/web/src/components/settings/attribute-mappings-section.tsx
index e37aa587..2505c705 100644
--- a/apps/web/src/components/settings/attribute-mappings-section.tsx
+++ b/apps/web/src/components/settings/attribute-mappings-section.tsx
@@ -21,14 +21,6 @@ import {
} from "@maple/ui/components/ui/alert-dialog"
import { Badge } from "@maple/ui/components/ui/badge"
import { Button } from "@maple/ui/components/ui/button"
-import {
- Card,
- CardAction,
- CardContent,
- CardDescription,
- CardHeader,
- CardTitle,
-} from "@maple/ui/components/ui/card"
import {
Dialog,
DialogContent,
@@ -68,6 +60,7 @@ import {
import { AttributeKeyAutocomplete } from "./attribute-key-autocomplete"
const MONO = "font-mono text-[0.92em] text-muted-foreground"
+const COL_HEADER = "text-muted-foreground/70 font-mono text-[10px] uppercase tracking-[0.12em]"
const SOURCE_CONTEXT_LABELS: Record = {
span: "Span attribute",
@@ -228,30 +221,32 @@ export function AttributeMappingsSection() {
return (
<>
-
-
-
- Attribute Mappings
- {mappingCount !== null && mappingCount > 0 && (
-
- {mappingCount}
-
- )}
-
-
- Rename or promote span attribute keys at ingest so telemetry from different SDKs stays
- consistent. Applied only to spans received after a rule is saved.
-
-
-
-
-
-
+
+
+
+
+ Attribute mappings
+ {mappingCount !== null && mappingCount > 0 && (
+
+ {" "}
+ · {mappingCount}
+
+ )}
+
+
+ Rename or promote span attribute keys at ingest. Applied only to spans received
+ after a rule is saved.
+
+
+
+
+
+
{Result.isInitial(listResult) ? (
-
+
{[0, 1].map((i) => (
@@ -292,32 +287,32 @@ export function AttributeMappingsSection() {
) : (
{/* column header */}
-
-
Name
-
Mapping
-
Operation
-
Added
+
+
Name
+
Rule
+
Operation
+
Context
+
{mappings.map((mapping) => {
const operation = OPERATION_BADGE[mapping.operation]
- const OperationIcon = operation.icon
return (
{mapping.name}
@@ -332,33 +327,23 @@ export function AttributeMappingsSection() {
{mapping.target_key}
- {mapping.source_context === "resource" && (
-
- · from {SOURCE_CONTEXT_LABELS.resource.toLowerCase()}
-
- )}
-
-
-
- {OPERATION_LABELS[mapping.operation]}
-
-
+
+ {OPERATION_LABELS[mapping.operation]}
+
-
-
handleToggleEnabled(mapping)}
- disabled={togglingId === mapping.id}
- />
-
- {formatRelativeTime(mapping.created_at)}
-
-
+
+ {mapping.source_context === "resource" ? "Resource" : "Spans"}
+
+
+
+
+
handleToggleEnabled(mapping)}
+ disabled={togglingId === mapping.id}
+ title={`Added ${formatRelativeTime(mapping.created_at)}`}
+ />
)
})}
)}
-
-
+
+
{/* Add / Edit Dialog */}