Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 12 additions & 7 deletions apps/mobile/src/app/(tabs)/inbox.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { buildInboxViewedProperties } from "@posthog/core/inbox/engagement";
import { INBOX_PIPELINE_STATUSES } from "@posthog/core/inbox/reportFiltering";
import type { SignalReport } from "@posthog/shared/domain-types";
import { useFocusEffect, useRouter } from "expo-router";
Expand All @@ -24,7 +25,6 @@ import {
} from "@/features/inbox/stores/dismissedReportsStore";
import { useInboxFilterStore } from "@/features/inbox/stores/inboxFilterStore";
import { useInboxStore } from "@/features/inbox/stores/inboxStore";
import { buildInboxViewedProperties } from "@/features/inbox/utils";
import { useIntegrations } from "@/features/tasks/hooks/useIntegrations";
import { ANALYTICS_EVENTS, useAnalytics } from "@/lib/analytics";

Expand Down Expand Up @@ -67,12 +67,17 @@ export default function InboxScreen() {
viewedFiredForFocusRef.current = focusVersion;
analytics.track(
ANALYTICS_EVENTS.INBOX_VIEWED,
buildInboxViewedProperties(reports, totalCount, {
sourceProductFilter,
statusFilter,
suggestedReviewerFilter,
priorityFilter,
defaultStatusFilter: INBOX_PIPELINE_STATUSES,
buildInboxViewedProperties({
visibleReports: reports,
totalCount,
filters: {
surface: "mobile",
sourceProductFilter,
statusFilter,
suggestedReviewerFilter,
priorityFilter,
defaultStatusFilter: INBOX_PIPELINE_STATUSES,
},
}),
);
}, [
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { Text } from "@components/text";
import { isRestorableReport } from "@posthog/core/inbox/reportMembership";
import { inboxStatusLabel } from "@posthog/core/inbox/reportPresentation";
import { dismissalReasonLabel } from "@posthog/shared";
import type { SignalReport } from "@posthog/shared/domain-types";
Expand All @@ -14,7 +15,7 @@ import {
} from "react-native";
import { useThemeColors } from "@/lib/theme";
import { useArchivedReports, useRestoreReport } from "../hooks/useInboxReports";
import { formatReportTimestamp, isRestorableReport } from "../utils";
import { formatReportTimestamp } from "../utils";

interface ArchivedReportListProps {
onReportPress?: (report: SignalReport) => void;
Expand Down
2 changes: 1 addition & 1 deletion apps/mobile/src/features/inbox/hooks/useInboxReports.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
INBOX_DISMISSED_STATUS_FILTER,
INBOX_REFETCH_INTERVAL_MS,
} from "@posthog/core/inbox/reportFiltering";
import { isRestorableReport } from "@posthog/core/inbox/reportMembership";
import type { DismissalReasonOptionValue } from "@posthog/shared";
import type {
AvailableSuggestedReviewersResponse,
Expand All @@ -31,7 +32,6 @@ import { useMemo } from "react";
import { useAuthStore } from "@/features/auth";
import { getPostHogApiClient } from "@/lib/posthogApiClient";
import { useInboxFilterStore } from "../stores/inboxFilterStore";
import { isRestorableReport } from "../utils";

export const inboxKeys = {
all: ["inbox", "signal-reports"] as const,
Expand Down
35 changes: 27 additions & 8 deletions apps/mobile/src/features/inbox/utils.test.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import { buildInboxViewedProperties } from "@posthog/core/inbox/engagement";
import {
buildArchiveListOrdering,
buildPriorityFilterParam,
buildSignalReportListOrdering,
INBOX_PIPELINE_STATUSES,
} from "@posthog/core/inbox/reportFiltering";
import { isRestorableReport } from "@posthog/core/inbox/reportMembership";
import { formatSignalReportSummaryMarkdown } from "@posthog/core/inbox/reportPresentation";
import { dismissalReasonLabel } from "@posthog/shared";
import type {
Expand All @@ -12,7 +14,24 @@ import type {
SignalReportStatus,
} from "@posthog/shared/domain-types";
import { describe, expect, it } from "vitest";
import { buildInboxViewedProperties, isRestorableReport } from "./utils";

function buildMobileInboxViewedProperties(
reports: SignalReport[],
totalCount: number,
filters: {
sourceProductFilter: string[];
statusFilter: readonly SignalReportStatus[];
suggestedReviewerFilter: string[];
priorityFilter: string[];
defaultStatusFilter: readonly SignalReportStatus[];
},
) {
return buildInboxViewedProperties({
visibleReports: reports,
totalCount,
filters: { surface: "mobile", ...filters },
});
}

function makeReport(
partial: Partial<SignalReport> & Pick<SignalReport, "id">,
Expand Down Expand Up @@ -70,7 +89,7 @@ describe("formatSignalReportSummaryMarkdown", () => {

describe("buildInboxViewedProperties", () => {
it("emits zero counts for an empty list", () => {
const props = buildInboxViewedProperties([], 0, {
const props = buildMobileInboxViewedProperties([], 0, {
sourceProductFilter: [],
statusFilter: INBOX_PIPELINE_STATUSES,
suggestedReviewerFilter: [],
Expand Down Expand Up @@ -119,7 +138,7 @@ describe("buildInboxViewedProperties", () => {
makeReport({ id: "4", status: "failed" }),
];

const props = buildInboxViewedProperties(reports, 4, {
const props = buildMobileInboxViewedProperties(reports, 4, {
sourceProductFilter: [],
statusFilter: INBOX_PIPELINE_STATUSES,
suggestedReviewerFilter: [],
Expand All @@ -140,7 +159,7 @@ describe("buildInboxViewedProperties", () => {
});

it("marks filters active when any of status/source/reviewer/priority differs from defaults", () => {
const narrowed = buildInboxViewedProperties([], 0, {
const narrowed = buildMobileInboxViewedProperties([], 0, {
sourceProductFilter: [],
statusFilter: ["ready"],
suggestedReviewerFilter: [],
Expand All @@ -150,7 +169,7 @@ describe("buildInboxViewedProperties", () => {
expect(narrowed.has_active_filters).toBe(true);
expect(narrowed.status_filter_count).toBe(1);

const sourced = buildInboxViewedProperties([], 0, {
const sourced = buildMobileInboxViewedProperties([], 0, {
sourceProductFilter: ["error_tracking"],
statusFilter: INBOX_PIPELINE_STATUSES,
suggestedReviewerFilter: [],
Expand All @@ -160,7 +179,7 @@ describe("buildInboxViewedProperties", () => {
expect(sourced.has_active_filters).toBe(true);
expect(sourced.source_product_filter).toEqual(["error_tracking"]);

const reviewer = buildInboxViewedProperties([], 0, {
const reviewer = buildMobileInboxViewedProperties([], 0, {
sourceProductFilter: [],
statusFilter: INBOX_PIPELINE_STATUSES,
suggestedReviewerFilter: ["uuid-1"],
Expand All @@ -169,7 +188,7 @@ describe("buildInboxViewedProperties", () => {
});
expect(reviewer.has_active_filters).toBe(true);

const prioritized = buildInboxViewedProperties([], 0, {
const prioritized = buildMobileInboxViewedProperties([], 0, {
sourceProductFilter: [],
statusFilter: INBOX_PIPELINE_STATUSES,
suggestedReviewerFilter: [],
Expand All @@ -180,7 +199,7 @@ describe("buildInboxViewedProperties", () => {
});

it("treats a reordered default status set as not filtered", () => {
const props = buildInboxViewedProperties([], 0, {
const props = buildMobileInboxViewedProperties([], 0, {
sourceProductFilter: [],
statusFilter: [...INBOX_PIPELINE_STATUSES].reverse(),
suggestedReviewerFilter: [],
Expand Down
119 changes: 1 addition & 118 deletions apps/mobile/src/features/inbox/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,8 @@ import {
EXTERNAL_INBOX_SOURCE_BY_PRODUCT,
type SourceProduct,
} from "@posthog/shared";
import type {
Signal,
SignalReport,
SignalReportPriority,
SignalReportStatus,
} from "@posthog/shared/domain-types";
import type { Signal } from "@posthog/shared/domain-types";
import { differenceInHours, format, formatDistanceToNow } from "date-fns";
import type { InboxViewedProperties } from "@/lib/analytics";

const ERROR_TRACKING_TYPE_LABELS: Record<string, string> = {
issue_created: "New issue",
Expand Down Expand Up @@ -47,120 +41,9 @@ export function sourceLine(signal: Signal): string {
const product = warehouseSource?.label ?? source_product.replace(/_/g, " ");
return `${product} · ${source_type.replace(/_/g, " ")}`;
}

/** Relative time for the last day, absolute "MMM d" beyond it. */
export function formatReportTimestamp(date: Date): string {
return differenceInHours(new Date(), date) < 24
? formatDistanceToNow(date, { addSuffix: true })
: format(date, "MMM d");
}

/**
* Archive membership: `suppressed` (user-archived) and `resolved` (PR merged).
* Only `suppressed` is restorable; `resolved` is terminal, shown for reference.
*/
export function isRestorableReport(
report: Pick<SignalReport, "status">,
): boolean {
return report.status === "suppressed";
}

/**
* Returns only reports that are actionable for the tinder-like card deck:
* ready, immediately actionable, not already addressed.
*/
export function getActionableReports(reports: SignalReport[]): SignalReport[] {
return reports.filter(
(r) =>
r.status === "ready" &&
r.actionability === "immediately_actionable" &&
!r.already_addressed,
);
}

interface InboxViewedFilterState {
sourceProductFilter: string[];
statusFilter: readonly SignalReportStatus[];
suggestedReviewerFilter: string[];
priorityFilter: SignalReportPriority[];
/** Default status filter as defined in the filter store, used to detect whether the user has narrowed it. */
defaultStatusFilter: readonly SignalReportStatus[];
}

/**
* Build the property payload for the `Inbox viewed` analytics event.
*
* Mirrors packages/ui/src/features/inbox/components/InboxSignalsTab.tsx so
* desktop and mobile send the same shape into PostHog.
*/
export function buildInboxViewedProperties(
reports: SignalReport[],
totalCount: number,
filters: InboxViewedFilterState,
): InboxViewedProperties {
const priorityCounts = {
P0: 0,
P1: 0,
P2: 0,
P3: 0,
P4: 0,
unknown: 0,
};
const actionabilityCounts = {
immediately_actionable: 0,
requires_human_input: 0,
not_actionable: 0,
unknown: 0,
};
let readyCount = 0;
for (const r of reports) {
if (r.status === "ready") readyCount += 1;
const p = r.priority;
if (p === "P0" || p === "P1" || p === "P2" || p === "P3" || p === "P4") {
priorityCounts[p] += 1;
} else {
priorityCounts.unknown += 1;
}
const a = r.actionability;
if (
a === "immediately_actionable" ||
a === "requires_human_input" ||
a === "not_actionable"
) {
actionabilityCounts[a] += 1;
} else {
actionabilityCounts.unknown += 1;
}
}

const statusFiltered =
filters.statusFilter.length !== filters.defaultStatusFilter.length ||
filters.statusFilter.some((s) => !filters.defaultStatusFilter.includes(s));
const hasActiveFilters =
statusFiltered ||
filters.sourceProductFilter.length > 0 ||
filters.suggestedReviewerFilter.length > 0 ||
filters.priorityFilter.length > 0;

return {
report_count: reports.length,
total_count: totalCount,
ready_count: readyCount,
has_active_filters: hasActiveFilters,
source_product_filter: filters.sourceProductFilter,
status_filter_count: filters.statusFilter.length,
is_empty: totalCount === 0,
priority_p0_count: priorityCounts.P0,
priority_p1_count: priorityCounts.P1,
priority_p2_count: priorityCounts.P2,
priority_p3_count: priorityCounts.P3,
priority_p4_count: priorityCounts.P4,
priority_unknown_count: priorityCounts.unknown,
actionability_immediately_actionable_count:
actionabilityCounts.immediately_actionable,
actionability_requires_human_input_count:
actionabilityCounts.requires_human_input,
actionability_not_actionable_count: actionabilityCounts.not_actionable,
actionability_unknown_count: actionabilityCounts.unknown,
};
}
5 changes: 1 addition & 4 deletions apps/mobile/src/features/tasks/skills/skillTemplateIds.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,7 @@ export function formatSkillTemplateId(skillName: string): string {
export function parseSkillTemplateId(
templateId: string | null | undefined,
): string | null {
if (!templateId?.startsWith(SKILL_TEMPLATE_ID_PREFIX)) {
return null;
}

if (!templateId?.startsWith(SKILL_TEMPLATE_ID_PREFIX)) return null;
const skillName = templateId.slice(SKILL_TEMPLATE_ID_PREFIX.length).trim();
return skillName || null;
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@ import { parseSkillTemplateId } from "../skills/skillTemplateIds";

export interface AutomationTemplatePresentation {
templateName: string | null;
repositoryLabel: string | null;
contextLabel: string | null;
repositoryLabel: string | null;
secondaryLabel: string;
}

Expand All @@ -14,12 +14,11 @@ export function getAutomationTemplatePresentation(
const repositoryLabel = automation.repository.trim() || null;
const skillName = parseSkillTemplateId(automation.template_id);
const contextLabel = skillName ? "Skill store" : null;

return {
templateName:
skillName ?? (automation.template_id ? "Template automation" : null),
repositoryLabel,
contextLabel,
repositoryLabel,
secondaryLabel: repositoryLabel ?? contextLabel ?? "No repository context",
};
}
1 change: 1 addition & 0 deletions packages/core/src/inbox/engagement.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ function fakeReport(overrides: Partial<SignalReport> = {}): SignalReport {
}

const NO_FILTERS = {
surface: "desktop" as const,
sourceProductFilter: [],
priorityFilter: [],
searchQuery: "",
Expand Down
Loading
Loading