Skip to content
Closed
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
14 changes: 14 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,20 @@ npx @posthog/wizard@latest revenue-analytics
Requires PostHog and Stripe SDKs already installed. Supports `--ci` with the
same flags as the main wizard.

### Transactional email

Send your first transactional email from your server code, such as a welcome
email after signup:

```bash
npx @posthog/wizard@latest transactional-email
```

The setup agent checks for a verified sender, asks for a project secret API key
and writes it to your env file, and adds the send with `posthog-node` or the
HTTP API. The transactional email API is in beta and must be enabled for your
project.

### Data Warehouse

Detect data sources your project already uses (Postgres, MySQL, MongoDB,
Expand Down
7 changes: 7 additions & 0 deletions src/cli/__tests__/programs-cli.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import { migrateCommand } from '../commands/migrate';
import { mcpAnalyticsCommand } from '../commands/mcp-analytics';
import { replayVisionCommand } from '../commands/replay-vision';
import { revenueCommand } from '../commands/revenue';
import { transactionalEmailCommand } from '../commands/transactional-email';
import { warehouseCommand } from '../commands/warehouse';
import { uploadSourcemapsCommand } from '../commands/upload-sourcemaps';
import { selfDrivingCommand } from '../commands/self-driving';
Expand Down Expand Up @@ -202,6 +203,12 @@ describe('flat skill commands', () => {
expect(config.skillId).toBe('revenue-analytics-setup');
});

test('transactional-email dispatches with omnibus-send-transactional-email skillId', () => {
transactionalEmailCommand.handler!(makeArgv({ debug: true }));
const [config] = mockRunWizard.mock.calls[0] as [{ skillId?: string }];
expect(config.skillId).toBe('omnibus-send-transactional-email');
});

test('mcp-analytics dispatches with mcp-analytics skillId', () => {
mcpAnalyticsCommand.handler!(makeArgv({ debug: true }));
const [config] = mockRunWizard.mock.calls[0] as [{ skillId?: string }];
Expand Down
9 changes: 9 additions & 0 deletions src/cli/commands/transactional-email.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { config as transactionalEmailConfig } from '@programs/transactional-email';

import type { Command } from './command';
import { nativeCommandFactory } from './factories/native-command-factory';

/** `wizard transactional-email`: send the first transactional email from the project's server code. */
export const transactionalEmailCommand: Command = nativeCommandFactory(
transactionalEmailConfig,
);
2 changes: 2 additions & 0 deletions src/cli/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import { slackCommand } from './commands/slack';
import { uploadSourcemapsCommand } from './commands/upload-sourcemaps';
import { errorTrackingCommand } from './commands/error-tracking';
import { featureFlagsCommand } from './commands/feature-flags';
import { transactionalEmailCommand } from './commands/transactional-email';
import { skillCommand } from './commands/skill';

/** Register every command and run the one `process.argv` names. */
Expand All @@ -38,6 +39,7 @@ export function runCli(): void {
.use(uploadSourcemapsCommand)
.use(errorTrackingCommand)
.use(featureFlagsCommand)
.use(transactionalEmailCommand)
.use(skillCommand)
.init();
}
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@ describe('program bindings', () => {
'error-tracking': orchestratorOnPi,
'feature-flags': orchestratorOnPi,
'agent-skill': linearOnSol,
'transactional-email': linearOnSol,
});
});
});
Expand Down
3 changes: 3 additions & 0 deletions src/programs/program-runner/program-registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import { config as sourceMaps } from '@programs/error-tracking-upload-source-map
import { config as errorTracking } from '@programs/error-tracking';
import { config as featureFlags } from '@programs/feature-flags';
import { config as agentSkill } from '@programs/agent-skill';
import { config as transactionalEmail } from '@programs/transactional-email';

const [audit, eventsAudit] = auditPrograms;

Expand All @@ -45,6 +46,7 @@ export const PROGRAM_REGISTRY = [
replayVision,
aiObservability,
metrics,
transactionalEmail,
] as const satisfies readonly ProgramConfig[];

/**
Expand All @@ -69,6 +71,7 @@ export const Program = {
ReplayVision: replayVision.id,
AiObservability: aiObservability.id,
Metrics: metrics.id,
TransactionalEmail: transactionalEmail.id,
} as const;

/**
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { TRANSACTIONAL_EMAIL_ABORT_CASES } from '@programs/transactional-email';

describe('TRANSACTIONAL_EMAIL_ABORT_CASES', () => {
// The exact `[ABORT] <reason>` strings of the send-transactional-email skill's
// "Inside the PostHog Wizard" section (context-mill), prefix stripped.
it.each(['transactional email api not enabled', 'no server code found'])(
'matches the "%s" abort reason exactly once',
(reason) => {
const matched = TRANSACTIONAL_EMAIL_ABORT_CASES.filter((c) =>
c.match.test(reason),
);
expect(matched).toHaveLength(1);
},
);
});
48 changes: 48 additions & 0 deletions src/programs/transactional-email/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import type { AbortCase } from '@agent/types';
import { createSkillProgram } from '../shared/skill-program';
import type { ProgramConfig } from '@programs/program-contract';

const SKILL_ID = 'omnibus-send-transactional-email';
const REPORT_FILE = 'posthog-transactional-email-report.md';

/** The `[ABORT]` reasons of the skill's "Inside the PostHog Wizard" section (context-mill). */
export const TRANSACTIONAL_EMAIL_ABORT_CASES: AbortCase[] = [
{
match: /^transactional email api not enabled$/i,
message: "Transactional email isn't enabled for this project",
body:
'The transactional email API is in beta, and PostHog turns it on per ' +
'project. Ask PostHog in the app to enable it for your project, then ' +
'run this command again.',
},
{
match: /^no server code found$/i,
message: 'No server code found',
body:
'A transactional email is sent from server code, because the send needs ' +
"a secret key. The setup agent didn't find server code in this project. " +
'Run this command again from the project that runs your server.',
},
];

/** `wizard transactional-email`: verify a sender, set the secret key, and write the first send from code. */
export const config: ProgramConfig = createSkillProgram({
skillId: SKILL_ID,
command: 'transactional-email',
id: 'transactional-email',
description: 'Send your first transactional email from code',
integrationLabel: 'transactional-email',
customPrompt:
`Set up transactional email in this project with the \`${SKILL_ID}\` ` +
'skill: verify a sender, set the project secret API key, and write the ' +
'first send. Follow the skill\'s "Inside the PostHog Wizard" section. ' +
`Write the final report to ./${REPORT_FILE}: the email the app sends, the ` +
'files you changed, the environment variables every deployment needs, ' +
'and what the person still has to do.',
successMessage: `Transactional email is set up. View the report at ./${REPORT_FILE}`,
reportFile: REPORT_FILE,
docsUrl: 'https://posthog.com/docs/workflows/transactional-email-api',
spinnerMessage: 'Setting up transactional email...',
estimatedDurationMinutes: 5,
abortCases: TRANSACTIONAL_EMAIL_ABORT_CASES,
});
4 changes: 4 additions & 0 deletions src/programs/transactional-email/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"extends": "../../../tsconfig.no-tui.json",
"references": [{ "path": "../tsconfig.core.json" }]
}
1 change: 1 addition & 0 deletions src/programs/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
{ "path": "mcp-analytics" }, { "path": "metrics" }, { "path": "migration" },
{ "path": "posthog-integration" }, { "path": "replay-vision" },
{ "path": "revenue-analytics" }, { "path": "self-driving" },
{ "path": "transactional-email" },
{ "path": "warehouse-source" }, { "path": "web-analytics-doctor" }
]
}
53 changes: 53 additions & 0 deletions src/tui/__tests__/__snapshots__/flow-traces.test.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -1016,6 +1016,59 @@ exports[`flow traces per program > slack (node) 1`] = `
}
`;

exports[`flow traces per program > transactional-email (node) 1`] = `
{
"events": [
{
"event": "screen health-check",
"from": "agent-skill-intro",
"program": "transactional-email",
},
{
"event": "screen auth",
"from": "health-check",
"program": "transactional-email",
},
{
"event": "screen run",
"from": "auth",
"program": "transactional-email",
},
{
"event": "screen ai-opt-in",
"from": "run",
"program": "transactional-email",
},
{
"event": "screen run",
"from": "ai-opt-in",
"program": "transactional-email",
},
{
"event": "screen outro",
"from": "run",
"program": "transactional-email",
},
{
"event": "screen exit",
"from": "outro",
"program": "transactional-email",
},
],
"program": "transactional-email",
"screens": [
"agent-skill-intro",
"health-check",
"auth",
"ai-opt-in",
"run",
"outro",
"exit",
],
"stoppedOn": null,
}
`;

exports[`flow traces per program > warehouse-source (node) 1`] = `
{
"events": [
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,7 @@ describe('introEntries', () => {
'ai-observability',
'metrics',
'revenue-analytics-setup',
'transactional-email',
]);
});

Expand Down
1 change: 1 addition & 0 deletions src/tui/programs/posthog-integration/intro-menu.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ const INTRO_ENTRIES = [
'ai-observability',
'metrics',
'revenue-analytics-setup',
'transactional-email',
];

/** One spell book row: the id the intro hands off to, the words that run it, and its help line. */
Expand Down