@mailmodo/cli 0.0.56-beta.pr58.108 → 0.0.56-beta.pr58.95
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.
- package/dist/commands/deploy/index.js +3 -5
- package/dist/commands/init/index.js +3 -11
- package/dist/commands/login/index.js +5 -2
- package/dist/lib/api-client.d.ts +0 -2
- package/dist/lib/api-client.js +2 -2
- package/dist/lib/base-command.d.ts +13 -10
- package/dist/lib/base-command.js +27 -86
- package/dist/lib/commands/login/output.d.ts +2 -2
- package/dist/lib/commands/login/output.js +18 -5
- package/dist/lib/config.d.ts +0 -2
- package/dist/lib/config.js +10 -19
- package/dist/lib/constants.d.ts +2 -3
- package/dist/lib/constants.js +5 -4
- package/dist/lib/messages.d.ts +0 -18
- package/dist/lib/messages.js +0 -38
- package/dist/lib/templates/missing-templates.d.ts +1 -1
- package/dist/lib/templates/missing-templates.js +2 -2
- package/dist/lib/yaml-config.d.ts +0 -1
- package/dist/lib/yaml-config.js +0 -8
- package/oclif.manifest.json +1 -168
- package/package.json +1 -1
- package/dist/commands/report/index.d.ts +0 -22
- package/dist/commands/report/index.js +0 -123
- package/dist/lib/commands/report/output-entries.d.ts +0 -2
- package/dist/lib/commands/report/output-entries.js +0 -59
- package/dist/lib/commands/report/output-timeseries.d.ts +0 -2
- package/dist/lib/commands/report/output-timeseries.js +0 -28
- package/dist/lib/commands/report/output.d.ts +0 -3
- package/dist/lib/commands/report/output.js +0 -56
- package/dist/lib/commands/report/payload.d.ts +0 -2
- package/dist/lib/commands/report/payload.js +0 -49
- package/dist/lib/commands/report/prompt.d.ts +0 -2
- package/dist/lib/commands/report/prompt.js +0 -82
- package/dist/lib/commands/report/types.d.ts +0 -97
- package/dist/lib/commands/report/types.js +0 -1
package/oclif.manifest.json
CHANGED
|
@@ -631,173 +631,6 @@
|
|
|
631
631
|
"index.js"
|
|
632
632
|
]
|
|
633
633
|
},
|
|
634
|
-
"report": {
|
|
635
|
-
"aliases": [],
|
|
636
|
-
"args": {},
|
|
637
|
-
"description": "Fetch an email analytics report",
|
|
638
|
-
"examples": [
|
|
639
|
-
"<%= config.bin %> report --preset last7d",
|
|
640
|
-
"<%= config.bin %> report --preset last30d --group-by emailId",
|
|
641
|
-
"<%= config.bin %> report --from YYYY-MM-DD --to YYYY-MM-DD --output timeseries",
|
|
642
|
-
"<%= config.bin %> report --preset last7d --output entries --page 1",
|
|
643
|
-
"<%= config.bin %> report --preset last30d --sequence welcome-flow --event opened",
|
|
644
|
-
"<%= config.bin %> report --preset last7d --json"
|
|
645
|
-
],
|
|
646
|
-
"flags": {
|
|
647
|
-
"json": {
|
|
648
|
-
"description": "Output as JSON",
|
|
649
|
-
"name": "json",
|
|
650
|
-
"allowNo": false,
|
|
651
|
-
"type": "boolean"
|
|
652
|
-
},
|
|
653
|
-
"yes": {
|
|
654
|
-
"char": "y",
|
|
655
|
-
"description": "Skip confirmation prompts",
|
|
656
|
-
"name": "yes",
|
|
657
|
-
"allowNo": false,
|
|
658
|
-
"type": "boolean"
|
|
659
|
-
},
|
|
660
|
-
"contact": {
|
|
661
|
-
"description": "Filter by contact email (repeatable)",
|
|
662
|
-
"name": "contact",
|
|
663
|
-
"hasDynamicHelp": false,
|
|
664
|
-
"multiple": true,
|
|
665
|
-
"type": "option"
|
|
666
|
-
},
|
|
667
|
-
"email-id": {
|
|
668
|
-
"description": "Filter by email template ID (repeatable)",
|
|
669
|
-
"name": "email-id",
|
|
670
|
-
"hasDynamicHelp": false,
|
|
671
|
-
"multiple": true,
|
|
672
|
-
"type": "option"
|
|
673
|
-
},
|
|
674
|
-
"event": {
|
|
675
|
-
"description": "Filter by event type (repeatable)",
|
|
676
|
-
"name": "event",
|
|
677
|
-
"hasDynamicHelp": false,
|
|
678
|
-
"multiple": true,
|
|
679
|
-
"options": [
|
|
680
|
-
"bounced",
|
|
681
|
-
"clicked",
|
|
682
|
-
"complained",
|
|
683
|
-
"delivered",
|
|
684
|
-
"opened",
|
|
685
|
-
"sent",
|
|
686
|
-
"skipped",
|
|
687
|
-
"unsubscribed"
|
|
688
|
-
],
|
|
689
|
-
"type": "option"
|
|
690
|
-
},
|
|
691
|
-
"from": {
|
|
692
|
-
"description": "Start of time range, inclusive (YYYY-MM-DD)",
|
|
693
|
-
"exclusive": [
|
|
694
|
-
"preset"
|
|
695
|
-
],
|
|
696
|
-
"name": "from",
|
|
697
|
-
"hasDynamicHelp": false,
|
|
698
|
-
"multiple": false,
|
|
699
|
-
"type": "option"
|
|
700
|
-
},
|
|
701
|
-
"group-by": {
|
|
702
|
-
"description": "Group results by dimension",
|
|
703
|
-
"name": "group-by",
|
|
704
|
-
"default": "none",
|
|
705
|
-
"hasDynamicHelp": false,
|
|
706
|
-
"multiple": false,
|
|
707
|
-
"options": [
|
|
708
|
-
"contact",
|
|
709
|
-
"day",
|
|
710
|
-
"emailId",
|
|
711
|
-
"hour",
|
|
712
|
-
"none",
|
|
713
|
-
"sequenceId",
|
|
714
|
-
"status"
|
|
715
|
-
],
|
|
716
|
-
"type": "option"
|
|
717
|
-
},
|
|
718
|
-
"limit": {
|
|
719
|
-
"description": "Entries per page, max 200 (entries output only)",
|
|
720
|
-
"name": "limit",
|
|
721
|
-
"default": 50,
|
|
722
|
-
"hasDynamicHelp": false,
|
|
723
|
-
"multiple": false,
|
|
724
|
-
"type": "option"
|
|
725
|
-
},
|
|
726
|
-
"output": {
|
|
727
|
-
"description": "Output shape: summary | entries | timeseries",
|
|
728
|
-
"name": "output",
|
|
729
|
-
"default": "summary",
|
|
730
|
-
"hasDynamicHelp": false,
|
|
731
|
-
"multiple": false,
|
|
732
|
-
"options": [
|
|
733
|
-
"entries",
|
|
734
|
-
"summary",
|
|
735
|
-
"timeseries"
|
|
736
|
-
],
|
|
737
|
-
"type": "option"
|
|
738
|
-
},
|
|
739
|
-
"page": {
|
|
740
|
-
"description": "Page number (entries output only)",
|
|
741
|
-
"name": "page",
|
|
742
|
-
"default": 1,
|
|
743
|
-
"hasDynamicHelp": false,
|
|
744
|
-
"multiple": false,
|
|
745
|
-
"type": "option"
|
|
746
|
-
},
|
|
747
|
-
"preset": {
|
|
748
|
-
"description": "Relative time range preset",
|
|
749
|
-
"exclusive": [
|
|
750
|
-
"from",
|
|
751
|
-
"to"
|
|
752
|
-
],
|
|
753
|
-
"name": "preset",
|
|
754
|
-
"hasDynamicHelp": false,
|
|
755
|
-
"multiple": false,
|
|
756
|
-
"options": [
|
|
757
|
-
"last30d",
|
|
758
|
-
"last7d",
|
|
759
|
-
"last90d",
|
|
760
|
-
"lastMonth",
|
|
761
|
-
"thisMonth",
|
|
762
|
-
"today",
|
|
763
|
-
"yesterday"
|
|
764
|
-
],
|
|
765
|
-
"type": "option"
|
|
766
|
-
},
|
|
767
|
-
"sequence": {
|
|
768
|
-
"description": "Filter by sequence ID (repeatable)",
|
|
769
|
-
"name": "sequence",
|
|
770
|
-
"hasDynamicHelp": false,
|
|
771
|
-
"multiple": true,
|
|
772
|
-
"type": "option"
|
|
773
|
-
},
|
|
774
|
-
"to": {
|
|
775
|
-
"description": "End of time range, exclusive (YYYY-MM-DD)",
|
|
776
|
-
"exclusive": [
|
|
777
|
-
"preset"
|
|
778
|
-
],
|
|
779
|
-
"name": "to",
|
|
780
|
-
"hasDynamicHelp": false,
|
|
781
|
-
"multiple": false,
|
|
782
|
-
"type": "option"
|
|
783
|
-
}
|
|
784
|
-
},
|
|
785
|
-
"hasDynamicHelp": false,
|
|
786
|
-
"hiddenAliases": [],
|
|
787
|
-
"id": "report",
|
|
788
|
-
"pluginAlias": "@mailmodo/cli",
|
|
789
|
-
"pluginName": "@mailmodo/cli",
|
|
790
|
-
"pluginType": "core",
|
|
791
|
-
"strict": true,
|
|
792
|
-
"enableJsonFlag": false,
|
|
793
|
-
"isESM": true,
|
|
794
|
-
"relativePath": [
|
|
795
|
-
"dist",
|
|
796
|
-
"commands",
|
|
797
|
-
"report",
|
|
798
|
-
"index.js"
|
|
799
|
-
]
|
|
800
|
-
},
|
|
801
634
|
"sdk": {
|
|
802
635
|
"aliases": [],
|
|
803
636
|
"args": {},
|
|
@@ -932,5 +765,5 @@
|
|
|
932
765
|
]
|
|
933
766
|
}
|
|
934
767
|
},
|
|
935
|
-
"version": "0.0.56-beta.pr58.
|
|
768
|
+
"version": "0.0.56-beta.pr58.95"
|
|
936
769
|
}
|
package/package.json
CHANGED
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
import { BaseCommand } from '../../lib/base-command.js';
|
|
2
|
-
export default class Report extends BaseCommand {
|
|
3
|
-
static description: string;
|
|
4
|
-
static examples: string[];
|
|
5
|
-
static flags: {
|
|
6
|
-
contact: import("@oclif/core/interfaces").OptionFlag<string[] | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
7
|
-
'email-id': import("@oclif/core/interfaces").OptionFlag<string[] | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
8
|
-
event: import("@oclif/core/interfaces").OptionFlag<string[] | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
9
|
-
from: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
10
|
-
'group-by': import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
11
|
-
limit: import("@oclif/core/interfaces").OptionFlag<number, import("@oclif/core/interfaces").CustomOptions>;
|
|
12
|
-
output: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
13
|
-
page: import("@oclif/core/interfaces").OptionFlag<number, import("@oclif/core/interfaces").CustomOptions>;
|
|
14
|
-
preset: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
15
|
-
sequence: import("@oclif/core/interfaces").OptionFlag<string[] | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
16
|
-
to: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
17
|
-
json: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
18
|
-
yes: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
19
|
-
};
|
|
20
|
-
run(): Promise<void>;
|
|
21
|
-
private makeCtx;
|
|
22
|
-
}
|
|
@@ -1,123 +0,0 @@
|
|
|
1
|
-
import { Flags } from '@oclif/core';
|
|
2
|
-
import { BaseCommand } from '../../lib/base-command.js';
|
|
3
|
-
import { API_ENDPOINTS } from '../../lib/constants.js';
|
|
4
|
-
import { REPORTS } from '../../lib/messages.js';
|
|
5
|
-
import { buildReportPayload } from '../../lib/commands/report/payload.js';
|
|
6
|
-
import { renderReport } from '../../lib/commands/report/output.js';
|
|
7
|
-
import { promptReportFlags } from '../../lib/commands/report/prompt.js';
|
|
8
|
-
export default class Report extends BaseCommand {
|
|
9
|
-
static description = 'Fetch an email analytics report';
|
|
10
|
-
static examples = [
|
|
11
|
-
'<%= config.bin %> report --preset last7d',
|
|
12
|
-
'<%= config.bin %> report --preset last30d --group-by emailId',
|
|
13
|
-
'<%= config.bin %> report --from YYYY-MM-DD --to YYYY-MM-DD --output timeseries',
|
|
14
|
-
'<%= config.bin %> report --preset last7d --output entries --page 1',
|
|
15
|
-
'<%= config.bin %> report --preset last30d --sequence welcome-flow --event opened',
|
|
16
|
-
'<%= config.bin %> report --preset last7d --json',
|
|
17
|
-
];
|
|
18
|
-
static flags = {
|
|
19
|
-
...BaseCommand.baseFlags,
|
|
20
|
-
contact: Flags.string({
|
|
21
|
-
description: 'Filter by contact email (repeatable)',
|
|
22
|
-
multiple: true,
|
|
23
|
-
}),
|
|
24
|
-
'email-id': Flags.string({
|
|
25
|
-
description: 'Filter by email template ID (repeatable)',
|
|
26
|
-
multiple: true,
|
|
27
|
-
}),
|
|
28
|
-
event: Flags.string({
|
|
29
|
-
description: 'Filter by event type (repeatable)',
|
|
30
|
-
multiple: true,
|
|
31
|
-
options: [
|
|
32
|
-
'bounced',
|
|
33
|
-
'clicked',
|
|
34
|
-
'complained',
|
|
35
|
-
'delivered',
|
|
36
|
-
'opened',
|
|
37
|
-
'sent',
|
|
38
|
-
'skipped',
|
|
39
|
-
'unsubscribed',
|
|
40
|
-
],
|
|
41
|
-
}),
|
|
42
|
-
from: Flags.string({
|
|
43
|
-
description: 'Start of time range, inclusive (YYYY-MM-DD)',
|
|
44
|
-
exclusive: ['preset'],
|
|
45
|
-
}),
|
|
46
|
-
'group-by': Flags.string({
|
|
47
|
-
default: 'none',
|
|
48
|
-
description: 'Group results by dimension',
|
|
49
|
-
options: [
|
|
50
|
-
'contact',
|
|
51
|
-
'day',
|
|
52
|
-
'emailId',
|
|
53
|
-
'hour',
|
|
54
|
-
'none',
|
|
55
|
-
'sequenceId',
|
|
56
|
-
'status',
|
|
57
|
-
],
|
|
58
|
-
}),
|
|
59
|
-
limit: Flags.integer({
|
|
60
|
-
default: 50,
|
|
61
|
-
description: 'Entries per page, max 200 (entries output only)',
|
|
62
|
-
max: 200,
|
|
63
|
-
}),
|
|
64
|
-
output: Flags.string({
|
|
65
|
-
default: 'summary',
|
|
66
|
-
description: 'Output shape: summary | entries | timeseries',
|
|
67
|
-
options: ['entries', 'summary', 'timeseries'],
|
|
68
|
-
}),
|
|
69
|
-
page: Flags.integer({
|
|
70
|
-
default: 1,
|
|
71
|
-
description: 'Page number (entries output only)',
|
|
72
|
-
}),
|
|
73
|
-
preset: Flags.string({
|
|
74
|
-
description: 'Relative time range preset',
|
|
75
|
-
exclusive: ['from', 'to'],
|
|
76
|
-
options: [
|
|
77
|
-
'last30d',
|
|
78
|
-
'last7d',
|
|
79
|
-
'last90d',
|
|
80
|
-
'lastMonth',
|
|
81
|
-
'thisMonth',
|
|
82
|
-
'today',
|
|
83
|
-
'yesterday',
|
|
84
|
-
],
|
|
85
|
-
}),
|
|
86
|
-
sequence: Flags.string({
|
|
87
|
-
description: 'Filter by sequence ID (repeatable)',
|
|
88
|
-
multiple: true,
|
|
89
|
-
}),
|
|
90
|
-
to: Flags.string({
|
|
91
|
-
description: 'End of time range, exclusive (YYYY-MM-DD)',
|
|
92
|
-
exclusive: ['preset'],
|
|
93
|
-
}),
|
|
94
|
-
};
|
|
95
|
-
async run() {
|
|
96
|
-
const { flags } = await this.parse(Report);
|
|
97
|
-
await this.ensureAuth();
|
|
98
|
-
if ((flags.from && !flags.to) || (!flags.from && flags.to)) {
|
|
99
|
-
this.error(REPORTS.FROM_TO_REQUIRED);
|
|
100
|
-
}
|
|
101
|
-
const resolved = flags.json
|
|
102
|
-
? flags
|
|
103
|
-
: await promptReportFlags(flags);
|
|
104
|
-
const ctx = this.makeCtx();
|
|
105
|
-
const payload = buildReportPayload(resolved, (m) => this.error(m));
|
|
106
|
-
const response = await ctx.spinner(REPORTS.FETCH_SPINNER, flags.json, () => ctx.post(API_ENDPOINTS.REPORTS, payload));
|
|
107
|
-
if (!response.ok)
|
|
108
|
-
ctx.onApiError(response);
|
|
109
|
-
if (flags.json) {
|
|
110
|
-
this.log(JSON.stringify(response.data, null, 2));
|
|
111
|
-
return;
|
|
112
|
-
}
|
|
113
|
-
renderReport(ctx, response.data, resolved.output);
|
|
114
|
-
}
|
|
115
|
-
makeCtx() {
|
|
116
|
-
return {
|
|
117
|
-
log: (msg) => this.log(msg),
|
|
118
|
-
onApiError: (r) => this.handleApiError(r),
|
|
119
|
-
post: (path, body) => this.apiClient.post(path, body),
|
|
120
|
-
spinner: (text, json, work) => this.withApiSpinner({ json, text }, work),
|
|
121
|
-
};
|
|
122
|
-
}
|
|
123
|
-
}
|
|
@@ -1,59 +0,0 @@
|
|
|
1
|
-
import chalk from 'chalk';
|
|
2
|
-
const TIME_W = 18;
|
|
3
|
-
const EMAIL_W = 24;
|
|
4
|
-
const SEQ_W = 12;
|
|
5
|
-
const STATUS_W = 14;
|
|
6
|
-
const CONTACT_COL = TIME_W + EMAIL_W + SEQ_W + STATUS_W;
|
|
7
|
-
function statusColor(status) {
|
|
8
|
-
switch (status) {
|
|
9
|
-
case 'bounced':
|
|
10
|
-
case 'complained': {
|
|
11
|
-
return chalk.red;
|
|
12
|
-
}
|
|
13
|
-
case 'clicked':
|
|
14
|
-
case 'delivered':
|
|
15
|
-
case 'opened':
|
|
16
|
-
case 'sent': {
|
|
17
|
-
return chalk.green;
|
|
18
|
-
}
|
|
19
|
-
case 'skipped': {
|
|
20
|
-
return chalk.yellow;
|
|
21
|
-
}
|
|
22
|
-
default: {
|
|
23
|
-
return chalk.white;
|
|
24
|
-
}
|
|
25
|
-
}
|
|
26
|
-
}
|
|
27
|
-
function truncate(s, width) {
|
|
28
|
-
return s.length > width ? s.slice(0, width - 1) + '…' : s;
|
|
29
|
-
}
|
|
30
|
-
function shortSeq(id) {
|
|
31
|
-
return id.length > 8 ? id.slice(0, 8) + '…' : id;
|
|
32
|
-
}
|
|
33
|
-
export function renderEntries(ctx, data) {
|
|
34
|
-
const { entries, limit, page, total } = data;
|
|
35
|
-
ctx.log(`\n ${'Time'.padEnd(TIME_W)}${'Template'.padEnd(EMAIL_W)}` +
|
|
36
|
-
`${'Sequence'.padEnd(SEQ_W)}${'Status'.padEnd(STATUS_W)}Contact`);
|
|
37
|
-
ctx.log(` ${'─'.repeat(CONTACT_COL + 28)}`);
|
|
38
|
-
if (!entries?.length) {
|
|
39
|
-
ctx.log(` ${chalk.dim('No entries found.')}`);
|
|
40
|
-
ctx.log('');
|
|
41
|
-
return;
|
|
42
|
-
}
|
|
43
|
-
for (const entry of entries) {
|
|
44
|
-
const time = (entry.timestamp || '').padEnd(TIME_W);
|
|
45
|
-
const email = truncate(entry.emailId || '', EMAIL_W - 2).padEnd(EMAIL_W);
|
|
46
|
-
const seq = chalk.dim(shortSeq(entry.sequenceId || '').padEnd(SEQ_W));
|
|
47
|
-
const status = statusColor(entry.status)((entry.status || '').padEnd(STATUS_W));
|
|
48
|
-
ctx.log(` ${time}${email}${seq}${status}${entry.contact || ''}`);
|
|
49
|
-
if (entry.reason) {
|
|
50
|
-
const label = entry.status === 'skipped' ? 'condition not met' : 'reason';
|
|
51
|
-
ctx.log(` ${' '.repeat(CONTACT_COL)}${chalk.dim(`└ ${label}: ${entry.reason}`)}`);
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
const totalPages = Math.ceil(total / limit);
|
|
55
|
-
ctx.log(`\n Page ${page} of ${totalPages} · ${total} total entries`);
|
|
56
|
-
if (page < totalPages)
|
|
57
|
-
ctx.log(` ${chalk.dim(`Next: --page ${page + 1}`)}`);
|
|
58
|
-
ctx.log('');
|
|
59
|
-
}
|
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
import chalk from 'chalk';
|
|
2
|
-
function fmtRate(n) {
|
|
3
|
-
return (n * 100).toFixed(1) + '%';
|
|
4
|
-
}
|
|
5
|
-
export function renderTimeseries(ctx, data) {
|
|
6
|
-
const { bucket, series, timeRange } = data;
|
|
7
|
-
const from = timeRange.from.slice(0, 10);
|
|
8
|
-
const to = timeRange.to.slice(0, 10);
|
|
9
|
-
ctx.log(`\n ${chalk.dim(`Bucket: ${bucket} (${from} → ${to})`)}`);
|
|
10
|
-
ctx.log(`\n ${'Time'.padEnd(22)}${'Sent'.padEnd(7)}${'Delivered'.padEnd(11)}${'Opened'.padEnd(8)}${'Clicked'.padEnd(9)}${'OpenRate'.padEnd(10)}${'ClickRate'.padEnd(10)}BounceRate`);
|
|
11
|
-
ctx.log(` ${'─'.repeat(87)}`);
|
|
12
|
-
if (!series?.length) {
|
|
13
|
-
ctx.log(` ${chalk.dim('No timeseries data.')}`);
|
|
14
|
-
ctx.log('');
|
|
15
|
-
return;
|
|
16
|
-
}
|
|
17
|
-
for (const point of series) {
|
|
18
|
-
ctx.log(` ${(point.ts || '').padEnd(22)}` +
|
|
19
|
-
`${String(point.sent).padEnd(7)}` +
|
|
20
|
-
`${String(point.delivered).padEnd(11)}` +
|
|
21
|
-
`${String(point.opened).padEnd(8)}` +
|
|
22
|
-
`${String(point.clicked).padEnd(9)}` +
|
|
23
|
-
`${fmtRate(point.openRate).padEnd(10)}` +
|
|
24
|
-
`${fmtRate(point.clickRate).padEnd(10)}` +
|
|
25
|
-
`${fmtRate(point.bounceRate)}`);
|
|
26
|
-
}
|
|
27
|
-
ctx.log('');
|
|
28
|
-
}
|
|
@@ -1,56 +0,0 @@
|
|
|
1
|
-
import chalk from 'chalk';
|
|
2
|
-
import { renderEntries } from './output-entries.js';
|
|
3
|
-
import { renderTimeseries } from './output-timeseries.js';
|
|
4
|
-
function fmtRate(n) {
|
|
5
|
-
return (n * 100).toFixed(1) + '%';
|
|
6
|
-
}
|
|
7
|
-
function statsColumns(s) {
|
|
8
|
-
return [
|
|
9
|
-
String(s.sent).padEnd(7),
|
|
10
|
-
String(s.delivered).padEnd(11),
|
|
11
|
-
String(s.opened).padEnd(8),
|
|
12
|
-
String(s.clicked).padEnd(9),
|
|
13
|
-
String(s.bounced).padEnd(9),
|
|
14
|
-
fmtRate(s.openRate).padEnd(10),
|
|
15
|
-
fmtRate(s.clickRate).padEnd(10),
|
|
16
|
-
fmtRate(s.bounceRate),
|
|
17
|
-
].join('');
|
|
18
|
-
}
|
|
19
|
-
function renderGroupedTable(ctx, data) {
|
|
20
|
-
ctx.log(`\n ${'Key'.padEnd(20)}${'Sent'.padEnd(7)}${'Delivered'.padEnd(11)}` +
|
|
21
|
-
`${'Opened'.padEnd(8)}${'Clicked'.padEnd(9)}${'Bounced'.padEnd(9)}` +
|
|
22
|
-
`${'OpenRate'.padEnd(10)}${'ClickRate'.padEnd(10)}BounceRate`);
|
|
23
|
-
ctx.log(` ${'─'.repeat(93)}`);
|
|
24
|
-
for (const g of data.groups) {
|
|
25
|
-
const key = g.key.slice(0, 19).padEnd(20);
|
|
26
|
-
ctx.log(` ${key}${statsColumns(g)}`);
|
|
27
|
-
}
|
|
28
|
-
ctx.log(` ${'─'.repeat(93)}`);
|
|
29
|
-
ctx.log(` ${chalk.bold('TOTAL'.padEnd(20))}${statsColumns(data.totals)}`);
|
|
30
|
-
}
|
|
31
|
-
function renderTotalsOnly(ctx, data) {
|
|
32
|
-
const t = data.totals;
|
|
33
|
-
ctx.log(`\n Sent: ${t.sent} Delivered: ${t.delivered} Opened: ${t.opened}` +
|
|
34
|
-
` Clicked: ${t.clicked} Bounced: ${t.bounced}`);
|
|
35
|
-
ctx.log(` Open rate: ${fmtRate(t.openRate)} Click rate: ${fmtRate(t.clickRate)}` +
|
|
36
|
-
` Bounce rate: ${fmtRate(t.bounceRate)}`);
|
|
37
|
-
}
|
|
38
|
-
export function renderSummary(ctx, data) {
|
|
39
|
-
const { from, to } = data.timeRange;
|
|
40
|
-
ctx.log(`\n ${chalk.dim(`${from.slice(0, 10)} → ${to.slice(0, 10)}`)}`);
|
|
41
|
-
if (data.groups.length > 0) {
|
|
42
|
-
renderGroupedTable(ctx, data);
|
|
43
|
-
}
|
|
44
|
-
else {
|
|
45
|
-
renderTotalsOnly(ctx, data);
|
|
46
|
-
}
|
|
47
|
-
ctx.log('');
|
|
48
|
-
}
|
|
49
|
-
export function renderReport(ctx, data, mode) {
|
|
50
|
-
if (mode === 'entries')
|
|
51
|
-
renderEntries(ctx, data);
|
|
52
|
-
else if (mode === 'timeseries')
|
|
53
|
-
renderTimeseries(ctx, data);
|
|
54
|
-
else
|
|
55
|
-
renderSummary(ctx, data);
|
|
56
|
-
}
|
|
@@ -1,49 +0,0 @@
|
|
|
1
|
-
import { REPORTS } from '../../messages.js';
|
|
2
|
-
function buildTimeRange(flags) {
|
|
3
|
-
if (flags.preset)
|
|
4
|
-
return { preset: flags.preset };
|
|
5
|
-
if (flags.from && flags.to)
|
|
6
|
-
return { from: flags.from, to: flags.to };
|
|
7
|
-
return undefined;
|
|
8
|
-
}
|
|
9
|
-
function buildFilters(flags) {
|
|
10
|
-
const f = {};
|
|
11
|
-
if (flags.sequence?.length)
|
|
12
|
-
f.sequenceIds = flags.sequence;
|
|
13
|
-
if (flags['email-id']?.length)
|
|
14
|
-
f.emailIds = flags['email-id'];
|
|
15
|
-
if (flags.contact?.length)
|
|
16
|
-
f.contactEmails = flags.contact;
|
|
17
|
-
if (flags.event?.length)
|
|
18
|
-
f.events = flags.event;
|
|
19
|
-
return Object.keys(f).length > 0 ? f : undefined;
|
|
20
|
-
}
|
|
21
|
-
function validateFilterSizes(flags, onError) {
|
|
22
|
-
const arrays = [
|
|
23
|
-
flags.sequence,
|
|
24
|
-
flags['email-id'],
|
|
25
|
-
flags.contact,
|
|
26
|
-
flags.event,
|
|
27
|
-
];
|
|
28
|
-
if (arrays.some((a) => a && a.length > 100))
|
|
29
|
-
onError(REPORTS.TOO_MANY_ITEMS);
|
|
30
|
-
}
|
|
31
|
-
export function buildReportPayload(flags, onError) {
|
|
32
|
-
validateFilterSizes(flags, onError);
|
|
33
|
-
const timeRange = buildTimeRange(flags);
|
|
34
|
-
if (!timeRange)
|
|
35
|
-
onError(REPORTS.TIME_RANGE_REQUIRED);
|
|
36
|
-
const payload = {
|
|
37
|
-
groupBy: flags['group-by'],
|
|
38
|
-
output: flags.output,
|
|
39
|
-
timeRange,
|
|
40
|
-
};
|
|
41
|
-
const filters = buildFilters(flags);
|
|
42
|
-
if (filters)
|
|
43
|
-
payload.filters = filters;
|
|
44
|
-
if (flags.output === 'entries') {
|
|
45
|
-
payload.page = flags.page;
|
|
46
|
-
payload.limit = flags.limit;
|
|
47
|
-
}
|
|
48
|
-
return payload;
|
|
49
|
-
}
|
|
@@ -1,82 +0,0 @@
|
|
|
1
|
-
import { input, select } from '@inquirer/prompts';
|
|
2
|
-
const PRESET_CHOICES = [
|
|
3
|
-
{ name: 'Today', value: 'today' },
|
|
4
|
-
{ name: 'Yesterday', value: 'yesterday' },
|
|
5
|
-
{ name: 'Last 7 days', value: 'last7d' },
|
|
6
|
-
{ name: 'Last 30 days', value: 'last30d' },
|
|
7
|
-
{ name: 'Last 90 days', value: 'last90d' },
|
|
8
|
-
{ name: 'This month', value: 'thisMonth' },
|
|
9
|
-
{ name: 'Last month', value: 'lastMonth' },
|
|
10
|
-
];
|
|
11
|
-
const OUTPUT_CHOICES = [
|
|
12
|
-
{ name: 'Summary — grouped aggregates', value: 'summary' },
|
|
13
|
-
{ name: 'Entries — paginated event log', value: 'entries' },
|
|
14
|
-
{ name: 'Timeseries — bucketed over time', value: 'timeseries' },
|
|
15
|
-
];
|
|
16
|
-
const GROUP_BY_CHOICES = [
|
|
17
|
-
{ name: 'None (totals only)', value: 'none' },
|
|
18
|
-
{ name: 'Email template', value: 'emailId' },
|
|
19
|
-
{ name: 'Sequence ID', value: 'sequenceId' },
|
|
20
|
-
{ name: 'Day', value: 'day' },
|
|
21
|
-
{ name: 'Hour', value: 'hour' },
|
|
22
|
-
{ name: 'Contact', value: 'contact' },
|
|
23
|
-
{ name: 'Status', value: 'status' },
|
|
24
|
-
];
|
|
25
|
-
function isValidIsoDate(value) {
|
|
26
|
-
if (!value?.trim())
|
|
27
|
-
return 'Date is required';
|
|
28
|
-
if (Number.isNaN(new Date(value).getTime()))
|
|
29
|
-
return 'Enter a valid date in YYYY-MM-DD format';
|
|
30
|
-
return true;
|
|
31
|
-
}
|
|
32
|
-
async function promptTimeRange() {
|
|
33
|
-
const usePreset = await select({
|
|
34
|
-
choices: [
|
|
35
|
-
{ name: 'Use a preset (today, last7d, last30d…)', value: true },
|
|
36
|
-
{ name: 'Enter a custom date range', value: false },
|
|
37
|
-
],
|
|
38
|
-
message: 'Time range:',
|
|
39
|
-
});
|
|
40
|
-
if (usePreset) {
|
|
41
|
-
const preset = await select({
|
|
42
|
-
choices: PRESET_CHOICES,
|
|
43
|
-
message: 'Preset:',
|
|
44
|
-
});
|
|
45
|
-
return { preset };
|
|
46
|
-
}
|
|
47
|
-
const from = await input({
|
|
48
|
-
message: 'Start date (YYYY-MM-DD):',
|
|
49
|
-
validate: isValidIsoDate,
|
|
50
|
-
});
|
|
51
|
-
const to = await input({
|
|
52
|
-
message: 'End date, exclusive (YYYY-MM-DD):',
|
|
53
|
-
validate(v) {
|
|
54
|
-
const check = isValidIsoDate(v);
|
|
55
|
-
if (check !== true)
|
|
56
|
-
return check;
|
|
57
|
-
if (new Date(v) <= new Date(from))
|
|
58
|
-
return 'End date must be after start date';
|
|
59
|
-
return true;
|
|
60
|
-
},
|
|
61
|
-
});
|
|
62
|
-
return { from, to };
|
|
63
|
-
}
|
|
64
|
-
export async function promptReportFlags(flags) {
|
|
65
|
-
const hasTimeRange = Boolean(flags.preset || (flags.from && flags.to));
|
|
66
|
-
if (hasTimeRange)
|
|
67
|
-
return flags;
|
|
68
|
-
const timeRange = await promptTimeRange();
|
|
69
|
-
const output = await select({
|
|
70
|
-
choices: OUTPUT_CHOICES,
|
|
71
|
-
default: flags.output,
|
|
72
|
-
message: 'Output mode:',
|
|
73
|
-
});
|
|
74
|
-
const groupBy = output === 'summary'
|
|
75
|
-
? await select({
|
|
76
|
-
choices: GROUP_BY_CHOICES,
|
|
77
|
-
default: flags['group-by'],
|
|
78
|
-
message: 'Group by:',
|
|
79
|
-
})
|
|
80
|
-
: 'none';
|
|
81
|
-
return { ...flags, ...timeRange, 'group-by': groupBy, output };
|
|
82
|
-
}
|