Skip to content

Commit e2fa1aa

Browse files
feature: Add list logs endpoint
1 parent f9764a9 commit e2fa1aa

File tree

10 files changed

+303
-3
lines changed

10 files changed

+303
-3
lines changed

dist/Types/Types/Logs/index.d.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export * from './Logs.js';

lib/Classes/Events.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,16 +7,18 @@ import {
77
} from '../Types/Events/index.js';
88

99
import Request from './common/Request.js';
10-
import { IEventClient } from '../Interfaces/index.js';
10+
import { IEventClient, ILogger } from '../Interfaces/index.js';
1111

1212
export default class EventClient
1313
extends NavigationThruPages<EventsList>
1414
implements IEventClient {
1515
request: Request;
16+
private logger: ILogger;
1617

17-
constructor(request: Request) {
18+
constructor(request: Request, logger: ILogger = console) {
1819
super(request);
1920
this.request = request;
21+
this.logger = logger;
2022
}
2123

2224
protected parseList(
@@ -31,6 +33,7 @@ export default class EventClient
3133
}
3234

3335
async get(domain: string, query?: EventsQuery) : Promise<EventsList> {
36+
this.logger.warn('"events.get" method is deprecated. Please use "logs.list" instead');
3437
return this.requestListWithPages(urljoin('/v3', domain, 'events'), query);
3538
}
3639
}

lib/Classes/Logs/LogsClient.ts

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
import urljoin from 'url-join';
2+
3+
import Request from '../common/Request.js';
4+
import {
5+
LogsEvent, LogsEventItem, LogsList, LogsParsedQuery, LogsQuery
6+
} from '../../Types/Logs/Logs.js';
7+
import { ILogsClient } from '../../Interfaces/Logs/ILogsClient.js';
8+
import APIError from '../common/Error.js';
9+
import { APIResponse } from '../../definitions.js';
10+
11+
export default class LogsClient implements ILogsClient {
12+
request: Request;
13+
14+
constructor(request: Request) {
15+
this.request = request;
16+
}
17+
18+
private parseListResponse(response: APIResponse): LogsList {
19+
const parsedResponse: LogsList = {
20+
start: new Date(response.body.start),
21+
end: new Date(response.body.end),
22+
status: response.status,
23+
pagination: response.body.pagination,
24+
items: response.body.items.map((item: LogsEvent) => {
25+
const responseItem: LogsEventItem = { ...item, '@timestamp': new Date(item['@timestamp']) };
26+
return responseItem;
27+
}),
28+
aggregates: response.body.aggregates
29+
};
30+
return parsedResponse;
31+
}
32+
33+
private prepareDate(date: Date): string {
34+
// 'Wed, 03 Dec 2025 00:00:00 -0000'
35+
const formattedDate = `${date.toUTCString().slice(0, 25)} -0000`;
36+
return formattedDate;
37+
}
38+
39+
private parseQuery(queryData: LogsQuery): LogsParsedQuery {
40+
const res: LogsParsedQuery = { ...queryData, start: '', end: '' };
41+
if (queryData.start) {
42+
res.start = this.prepareDate(queryData.start);
43+
}
44+
if (queryData.end) {
45+
res.end = this.prepareDate(queryData.end);
46+
}
47+
return res;
48+
}
49+
50+
private validateQuery(queryData: LogsQuery): void {
51+
if (!queryData) {
52+
throw APIError.getUserDataError('Missed parameter', '"logs.list": Query data is required');
53+
}
54+
55+
if (queryData?.start) {
56+
if ((!(queryData?.start instanceof Date) || Number.isNaN(queryData.start.getTime()))) {
57+
throw APIError.getUserDataError('Incorrect type', '"logs.list": Type of "start" must be valid JS Data object');
58+
}
59+
} else {
60+
throw APIError.getUserDataError('Missed property', '"logs.list": "start" property is required');
61+
}
62+
63+
if (queryData?.end) {
64+
if ((!(queryData?.end instanceof Date) || Number.isNaN(queryData.end.getTime()))) {
65+
throw APIError.getUserDataError('Incorrect type', '"logs.list": Type of "end" must be valid JS Data object');
66+
}
67+
}
68+
69+
if (queryData.filter) {
70+
if (!queryData.filter.AND) {
71+
throw APIError.getUserDataError('Incorrect filter', '"logs.list": Logs filter must have AND operator');
72+
}
73+
if (!Array.isArray(queryData.filter.AND) || queryData.filter.AND.length === 0) {
74+
throw APIError.getUserDataError('Incorrect filter', '"logs.list": Logs filter AND operator must be an array');
75+
}
76+
77+
queryData.filter.AND.forEach((condition) => {
78+
if (!condition.attribute || !condition.comparator || !condition.values) {
79+
throw APIError.getUserDataError('Incorrect filter', '"logs.list": Each condition in Logs filter AND operator must have attribute, comparator and values');
80+
}
81+
if (!Array.isArray(condition.values) || condition.values.length === 0) {
82+
throw APIError.getUserDataError('Incorrect filter', '"logs.list": Values in each condition of Logs filter AND operator must be an array');
83+
}
84+
85+
condition.values.forEach((value) => {
86+
if (!value.label || !value.value) {
87+
throw APIError.getUserDataError('Incorrect filter', '"logs.list": Each value in Logs filter condition must have label and value');
88+
}
89+
});
90+
});
91+
}
92+
}
93+
94+
async list(queryData: LogsQuery) : Promise<LogsList> {
95+
this.validateQuery(queryData);
96+
const preparedQuery = this.parseQuery(queryData);
97+
const response = await this.request.post(urljoin('/v1/analytics/logs'), preparedQuery);
98+
return this.parseListResponse(response);
99+
}
100+
}

lib/Classes/MailgunClient.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,8 @@ import InboxPlacementsProvidersClient from './InboxPlacements/providers/InboxPla
4747
import MetricsClient from './Metrics/MetricsClient.js';
4848
import DomainTrackingClient from './Domains/domainsTracking.js';
4949
import DomainKeysClient from './Domains/domainsKeys.js';
50+
import LogsClient from './Logs/LogsClient.js';
51+
import { ILogsClient } from '../Interfaces/Logs/ILogsClient.js';
5052

5153
export default class MailgunClient implements IMailgunClient {
5254
public request;
@@ -65,6 +67,7 @@ export default class MailgunClient implements IMailgunClient {
6567
public lists: IMailingListsClient;
6668
public subaccounts: ISubaccountsClient;
6769
public inboxPlacements: IInboxPlacementsClient;
70+
public logs: ILogsClient;
6871

6972
constructor(options: MailgunClientOptions, formData: InputFormData) {
7073
const config: RequestOptions = { ...options } as RequestOptions;
@@ -145,6 +148,7 @@ export default class MailgunClient implements IMailgunClient {
145148
inboxPlacementsResultsClient,
146149
inboxPlacementsProvidersClient,
147150
);
151+
this.logs = new LogsClient(this.request);
148152
}
149153

150154
setSubaccount(subaccountId: string): void {

lib/Interfaces/Logs/ILogsClient.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
/* eslint-disable camelcase */
2+
3+
import { LogsList, LogsQuery } from '../../Types/Logs/index.js';
4+
5+
export interface ILogsClient {
6+
list(query: LogsQuery) : Promise<LogsList>
7+
}

lib/Interfaces/Logs/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export * from './ILogsClient.js';

lib/Interfaces/MailgunClient/IMailgunClient.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import { ISubaccountsClient } from '../Subaccounts/index.js';
1414
import { IInboxPlacementsClient } from '../InboxPlacements/index.js';
1515
import { IMetricsClient } from '../Metrics/MetricsClient.js';
1616
import type Request from '../../Classes/common/Request.js';
17+
import { ILogsClient } from '../Logs/ILogsClient.js';
1718

1819
export interface IMailgunClient {
1920
request: Request;
@@ -33,4 +34,5 @@ export interface IMailgunClient {
3334
inboxPlacements: IInboxPlacementsClient;
3435
setSubaccount(subaccountId: string): void;
3536
resetSubaccount(): void;
37+
logs: ILogsClient
3638
}

lib/Types/Common/RequestOptions.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import type { ValidationQuery } from '../Validations/index.js';
2424
import type { IpPoolDeleteData } from '../IPPools/index.js';
2525
import type { MetricsQuery } from '../Metrics/index.js';
2626
import type { FormDataInput } from './FormData.js';
27+
import { LogsQuery } from '../Logs/Logs.js';
2728

2829
export type OnCallEmptyHeaders = {
2930
[key: string]: undefined;
@@ -70,7 +71,7 @@ export type GetQueryTypes = IPsListQuery |
7071

7172
export type DeleteQueryTypes = DeletedDomainKeysQuery;
7273

73-
export type PostDataTypes = InboxPlacementsData | MetricsQuery | string;
74+
export type PostDataTypes = InboxPlacementsData | MetricsQuery | LogsQuery |string;
7475
export type PutDataTypes = SeedsListsUpdatingData |
7576
object |
7677
FormDataInput |

lib/Types/Logs/Logs.ts

Lines changed: 180 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,180 @@
1+
export type LogsFilterValue = {
2+
label: string;
3+
value: string;
4+
}
5+
6+
export type LogsFilter = {
7+
AND: {
8+
attribute: string;
9+
comparator: string;
10+
values: LogsFilterValue[];
11+
}[]
12+
}
13+
14+
export type LogsQuery = {
15+
start?: Date;
16+
end?: Date;
17+
events?: string[];
18+
'metric_events'?: string[]
19+
'include_subaccounts'?: boolean;
20+
'include_totals'?: boolean;
21+
pagination?: {
22+
sort?: string;
23+
token?: string;
24+
limit?: number;
25+
},
26+
filter?: LogsFilter
27+
}
28+
29+
export type LogsParsedQuery = Omit<LogsQuery, 'start' | 'end'> & {
30+
start?: string;
31+
end?: string;
32+
}
33+
34+
export type LogsDeliveryStatus = {
35+
message?: string;
36+
'attempt-no'?: number;
37+
'code'?: number;
38+
'bounce-type'?: string;
39+
description?: string;
40+
'session-seconds'?: number;
41+
'retry-seconds'?: number;
42+
'enhanced-code'?: string;
43+
'mx-host'?: string;
44+
'certificate-verified'?: boolean;
45+
'tls'?: boolean;
46+
'utf8'?: boolean;
47+
'first-delivery-attempt-seconds'?: number;
48+
'last-code'?: number;
49+
'last-message'?: string;
50+
}
51+
52+
export type LogsEvent = {
53+
id: string;
54+
event: string;
55+
'@timestamp': string;
56+
account?: {
57+
'parent-id': string;
58+
'id': string;
59+
};
60+
campaigns?: {
61+
id: string;
62+
name: string;
63+
}[]
64+
tags?: string[];
65+
method?: string;
66+
'originating-ip'?: string;
67+
'api-key-id'?: string;
68+
'delivery-status'?: LogsDeliveryStatus
69+
'i-delivery-optimizer'?: string;
70+
domain: {
71+
name: string;
72+
}
73+
recipient?: string;
74+
'recipient-domain'?: string;
75+
'recipient-provider'?: string;
76+
envelope?: {
77+
sender?: string;
78+
transport?: string;
79+
'sending-ip'?: string;
80+
targets?: string;
81+
'i-ip-pool-id'?: string;
82+
};
83+
storage?: {
84+
region?: string;
85+
env?: string;
86+
key?: string;
87+
url?: string[];
88+
};
89+
template?: {
90+
name?: string;
91+
version?: string;
92+
'is-text'?: boolean;
93+
}
94+
'log-level'?: string;
95+
'user-variables'?: string;
96+
'message'?: {
97+
headers?: {
98+
to: string;
99+
'message-id': string;
100+
from: string;
101+
subject: string;
102+
};
103+
attachments?: {
104+
filename?: string
105+
'content-type'?: string
106+
size?: number
107+
}[];
108+
recipients?: string[];
109+
size?: number;
110+
'scheduled-for'?: number;
111+
}
112+
flags?: {
113+
'is-authenticated': boolean;
114+
'is-system-test': boolean;
115+
'is-routed': boolean;
116+
'is-amp'?: boolean;
117+
'is-test-mode': boolean;
118+
'is-delayed-bounce': boolean;
119+
'is-callback': boolean;
120+
'is-encrypted': boolean;
121+
};
122+
'primary-dkim'?: string;
123+
ip?: string;
124+
geolocation?: {
125+
city?: string;
126+
country?: string;
127+
region?: string;
128+
timezone?: string;
129+
}
130+
'client-info'?: {
131+
'client-name'?: string
132+
'client-os'?: string
133+
'client-type'?: string
134+
'device-type'?: string
135+
'user-agent'?: string
136+
ip?: string
137+
bot?: string
138+
}
139+
severity?: string;
140+
reason?: string
141+
routes?: {
142+
actions?: string;
143+
description?: string;
144+
expression?: string;
145+
id?: string;
146+
priority?: number;
147+
match?: {
148+
recipient?: string;
149+
}
150+
}
151+
'mailing-list'?: {
152+
address?: string;
153+
'list-id'?: string;
154+
sid?: string;
155+
}
156+
url?: string;
157+
}
158+
159+
export type LogsEventItem = Omit<LogsEvent, '@timestamp' > & {
160+
'@timestamp': Date;
161+
}
162+
163+
export type LogsList = {
164+
start: Date;
165+
end: Date;
166+
items: LogsEventItem[];
167+
pagination: {
168+
// all optional since not always returned
169+
previous?: string;
170+
first?: string;
171+
last?: string;
172+
next?: string;
173+
total?: number
174+
};
175+
aggregates: {
176+
all: number;
177+
metrics: object;
178+
};
179+
status: number
180+
}

lib/Types/Logs/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export * from './Logs.js';

0 commit comments

Comments
 (0)