Skip to content

Commit

Permalink
DEVPROD-9937 Update readme about analytics best practices (#344)
Browse files Browse the repository at this point in the history
  • Loading branch information
khelif96 authored Sep 4, 2024
1 parent 432b5c4 commit 78eff95
Show file tree
Hide file tree
Showing 15 changed files with 124 additions and 40 deletions.
79 changes: 79 additions & 0 deletions ANALYTICS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
# Analytics Best Practices

Analytics are a crucial component of tracking and understanding user behavior,
system performance, and other key metrics. To ensure that our analytics are
effective and actionable, we follow a set of best practices. These practices
help us maintain consistency, accuracy, and clarity in our analytics data.

When adding or updating a new analytic event utilize the following spreadsheet
to ensure that the event is consistent with the rest of the events in the
system:
[Analytics Event Spreadsheet](https://docs.google.com/spreadsheets/d/1s4_nq8ZiphXp5Uq_-9HT6GPqz-KOyaq6HuvmXYaSNzg/edit?gid=0#gid=0)

We utilize
[Honeycomb for web](https://docs.honeycomb.io/send-data/javascript-browser/honeycomb-distribution/)
to track analytic events. Using span attributes in OpenTelemetry, we can provide
additional context to our analytics data. Below are some best practices for
defining span attributes in OpenTelemetry.

## OpenTelemetry Span Attribute Best Practices

### 1. Use Namespaces for Context

When defining span attributes, it's essential to provide context by using
namespaces. This helps to clarify the origin and purpose of the attribute,
making it easier to interpret in analytics. Read more about Namespaces
[here](https://docs.honeycomb.io/get-started/best-practices/organizing-data/#namespace-custom-fields)

#### Bad Examples:

- `status`
- `identifier`
- `function.name`
- `function.status`

#### Good Examples:

- `task.status`
- `version.status`
- `project.identifier`
- `section.function.name`
- `section.function.status`

By including a namespace (e.g., `task`, `version`, `project`, `section`), the
attribute is contextualized, making it clear where it fits within the larger
system. It also allows for more straightforward data filtering and analysis.
e.g. `task.status=success` or `project.identifier=evergreen`.

### 2. Avoid Excessive Dot Nesting

While namespaces are useful, over-nesting with too many dots can lead to overly
complex attribute names. This not only makes them harder to read but can also
complicate data queries and analysis.

#### Bad Examples:

- `host.is.volume.migration`
- `host.is.unexpirable`

#### Good Examples:

- `host.is_volume_migration`
- `host.is_unexpirable`

In these examples, using underscores instead of excessive dot nesting keeps the
attribute names concise and easier to manage.

### 3. Snake case

When defining multi word attributes, use `snake_case` to separate words. This
ensures that the attribute is readable and easy to understand.

### 4. Strive for Balance

When creating span attributes, aim to balance the need for context with
simplicity. Use namespaces thoughtfully to provide clarity without
overcomplicating the attribute structure. This approach will help maintain both
readability and utility in our analytics. Read more about OpenTelemetry
attribute naming
[here](https://opentelemetry.io/docs/specs/semconv/general/attribute-naming/).
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@

The new home of [Spruce](/apps/spruce) and [Parsley](/apps/parsley).

## Analytics

Read more about our analytics practices [here](ANALYTICS.md).

## Monorepo Tips & Tricks

Learn about our monorepo shared library [here](packages/lib/README.md).
Expand Down
10 changes: 5 additions & 5 deletions apps/parsley/src/analytics/logWindow/useLogWindowAnalytics.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,14 +44,14 @@ type Action =
| { name: "Clicked close all sections button" }
| {
name: "Clicked open subsections button";
"function.name": string;
"function.status": SectionStatus;
"was.function.closed": boolean;
"section.function.name": string;
"section.function.status": SectionStatus;
"section.function.was_closed": boolean;
}
| {
name: "Clicked close subsections button";
"function.name": string;
"function.status": SectionStatus;
"section.function.name": string;
"section.function.status": SectionStatus;
}
| {
name: "Viewed log with sections and jump to failing line";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -83,10 +83,10 @@ describe("SectionControls", () => {
await user.click(screen.getByDataCy("open-subsections-btn"));
expect(sendEventMock).toHaveBeenCalledOnce();
expect(sendEventMock).toHaveBeenCalledWith({
"function.name": "funcName",
"function.status": "pass",
name: "Clicked open subsections button",
"was.function.closed": false,
"section.function.name": "funcName",
"section.function.status": "pass",
"section.function.was_closed": false,
});
expect(toggleAllCommandsInFunctionMock).toHaveBeenCalledOnce();
expect(toggleAllCommandsInFunctionMock).toHaveBeenCalledWith(
Expand All @@ -102,9 +102,9 @@ describe("SectionControls", () => {
);
expect(sendEventMock).toHaveBeenCalledTimes(2);
expect(sendEventMock).toHaveBeenCalledWith({
"function.name": "funcName",
"function.status": "pass",
name: "Clicked close subsections button",
"section.function.name": "funcName",
"section.function.status": "pass",
});
});
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,10 +30,10 @@ const SubsectionControls: React.FC<{
data-cy="open-subsections-btn"
onClick={() => {
sendEvent({
"function.name": functionName,
"function.status": status,
name: "Clicked open subsections button",
"was.function.closed": !sectionState[functionID].isOpen,
"section.function.name": functionName,
"section.function.status": status,
"section.function.was_closed": !sectionState[functionID].isOpen,
});
toggleAllCommandsInFunction(functionID, true);
}}
Expand All @@ -48,9 +48,9 @@ const SubsectionControls: React.FC<{
onClick={() => {
toggleAllCommandsInFunction(functionID, false);
sendEvent({
"function.name": functionName,
"function.status": status,
name: "Clicked close subsections button",
"section.function.name": functionName,
"section.function.status": status,
});
}}
size="xsmall"
Expand Down
10 changes: 5 additions & 5 deletions apps/spruce/src/analytics/spawn/useSpawnAnalytics.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,10 @@ type Action =
}
| {
name: "Created a spawn host";
"host.is.volume.migration": boolean;
"host.is.workstation": boolean;
"host.is_volume_migration": boolean;
"host.is_workstation": boolean;
"host.distro.id": string;
"host.is.unexpirable": boolean;
"host.is_unexpirable": boolean;
}
| { name: "Viewed spawn volume modal" }
| {
Expand All @@ -33,11 +33,11 @@ type Action =
name: "Created a volume";
"volume.type": string;
"volume.size": number;
"volume.is.unexpirable": boolean;
"volume.is_unexpirable": boolean;
}
| {
name: "Changed spawn volume settings";
"volume.is.unexpirable": boolean;
"volume.is_unexpirable": boolean;
}
| { name: "Clicked open IDE button" }
| { name: "Changed tab"; tab: string };
Expand Down
10 changes: 5 additions & 5 deletions apps/spruce/src/analytics/task/useTaskAnalytics.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,12 +27,12 @@ type Action =
}
| {
name: "Clicked restart task button";
"task.is.display_task": false;
"task.is_display_task": false;
}
| {
name: "Clicked restart task button";
allTasks: boolean;
"task.is.display_task": true;
"task.is_display_task": true;
}
| {
name: "Clicked execution tasks table link";
Expand Down Expand Up @@ -62,7 +62,7 @@ type Action =
| { name: "Clicked metadata link"; "link.type": string }
| {
name: "Clicked task file link";
"parsley.available": boolean;
"parsley.is_available": boolean;
"file.name": string;
}
| {
Expand Down Expand Up @@ -95,9 +95,9 @@ export const useTaskAnalytics = () => {
return useAnalyticsRoot<Action, AnalyticsIdentifier>("Task", {
"task.status": taskStatus || "",
"task.execution": execution,
"task.isLatestExecution": isLatestExecution,
"task.is_latest_execution": isLatestExecution,
"task.id": taskId || "",
"task.failedTestCount": failedTestCount || "",
"task.failed_test_count": failedTestCount || "",
"task.project.identifier": identifier,
});
};
2 changes: 1 addition & 1 deletion apps/spruce/src/analytics/version/useVersionAnalytics.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ type Action =
| { name: "Clicked metadata github commit link" }
| {
name: "Filtered by build variant and task status group";
taskSquareStatuses: string | string[];
"filter.task_square_statuses": string | string[];
}
| { name: "Clicked metadata previous version link" }
| { name: "Clicked metadata project patches link" }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -139,10 +139,10 @@ export const SpawnHostModal: React.FC<SpawnHostModalProps> = ({
});
spawnAnalytics.sendEvent({
name: "Created a spawn host",
"host.is.volume.migration": false,
"host.is.workstation": selectedDistro?.isVirtualWorkStation || false,
"host.is_volume_migration": false,
"host.is_workstation": selectedDistro?.isVirtualWorkStation || false,
"host.distro.id": selectedDistro?.name || "",
"host.is.unexpirable": mutationInput?.noExpiration || false,
"host.is_unexpirable": mutationInput?.noExpiration || false,
});
spawnHostMutation({
variables: { spawnHostInput: mutationInput },
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ export const SpawnVolumeModal: React.FC<SpawnVolumeModalProps> = ({
name: "Created a volume",
"volume.type": mutationInput.type,
"volume.size": mutationInput.size,
"volume.is.unexpirable": mutationInput.noExpiration || false,
"volume.is_unexpirable": mutationInput.noExpiration || false,
});
spawnVolumeMutation({
variables: { spawnVolumeInput: mutationInput },
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ export const EditVolumeModal: React.FC<Props> = ({
const mutationInput = formToGql(initialState, formState, volume.id);
spawnAnalytics.sendEvent({
name: "Changed spawn volume settings",
"volume.is.unexpirable": mutationInput.noExpiration,
"volume.is_unexpirable": mutationInput.noExpiration,
});
updateVolumeMutation({
variables: { updateVolumeInput: mutationInput },
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -106,10 +106,10 @@ export const MigrateVolumeModal: React.FC<MigrateVolumeModalProps> = ({
});
sendEvent({
name: "Created a spawn host",
"host.is.volume.migration": true,
"host.is_volume_migration": true,
"host.distro.id": mutationInput?.distroId || "",
"host.is.unexpirable": mutationInput?.noExpiration || false,
"host.is.workstation": mutationInput?.isVirtualWorkStation || false,
"host.is_unexpirable": mutationInput?.noExpiration || false,
"host.is_workstation": mutationInput?.isVirtualWorkStation || false,
});
migrateVolumeMutation({
variables: {
Expand Down
6 changes: 3 additions & 3 deletions apps/spruce/src/pages/task/ActionButtons.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -311,7 +311,7 @@ export const ActionButtons: React.FC<Props> = ({
taskAnalytics.sendEvent({
name: "Clicked restart task button",
allTasks: true,
"task.is.display_task": true,
"task.is_display_task": true,
});
}}
>
Expand All @@ -324,7 +324,7 @@ export const ActionButtons: React.FC<Props> = ({
taskAnalytics.sendEvent({
name: "Clicked restart task button",
allTasks: false,
"task.is.display_task": true,
"task.is_display_task": true,
});
}}
>
Expand All @@ -342,7 +342,7 @@ export const ActionButtons: React.FC<Props> = ({
restartTask({ variables: { taskId, failedOnly: false } });
taskAnalytics.sendEvent({
name: "Clicked restart task button",
"task.is.display_task": false,
"task.is_display_task": false,
});
}}
size="small"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ const columns = (
onClick={() => {
taskAnalytics.sendEvent({
name: "Clicked task file link",
"parsley.available": value.row.original.urlParsley !== null,
"parsley.is_available": value.row.original.urlParsley !== null,
"file.name": fileName,
});
}}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@ const VariantTaskGroup: React.FC<VariantTaskGroupProps> = ({
<GroupedTaskStatusBadge
key={`${versionId}_${variant}_${umbrellaStatus}`}
count={count}
// If the badge is active it should reset the page.
href={getVersionRoute(
versionId,
shouldLinkToVariant
Expand All @@ -106,10 +107,10 @@ const VariantTaskGroup: React.FC<VariantTaskGroupProps> = ({
onClick={() => {
sendEvent({
name: "Filtered by build variant and task status group",
taskSquareStatuses: Object.keys(groupedStatusCounts),
"filter.task_square_statuses":
Object.keys(groupedStatusCounts),
});
}}
// If the badge is active it should reset the page.
status={umbrellaStatus}
statusCounts={groupedStatusCounts}
/>
Expand Down

0 comments on commit 78eff95

Please sign in to comment.