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
2 changes: 0 additions & 2 deletions .env.sample
Original file line number Diff line number Diff line change
Expand Up @@ -77,8 +77,6 @@ CLOUDPAYMENTS_SECRET=
# INN of legal entity for CloudKassir
LEGAL_ENTITY_INN=

# Token for Amplitude analytics
AMPLITUDE_TOKEN=

# AWS S3 Bucket Configuration
AWS_S3_ACCESS_KEY_ID=
Expand Down
3 changes: 3 additions & 0 deletions index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@ if (process.env.HAWK_CATCHER_TOKEN) {
HawkCatcher.init({
token: process.env.HAWK_CATCHER_TOKEN,
release: `${name}-${version}`,
breadcrumbs: {
maxBreadcrumbs: 100
}
});
}

Expand Down
7 changes: 3 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "hawk.api",
"version": "1.4.2",
"version": "1.4.6",
"main": "index.ts",
"license": "BUSL-1.1",
"scripts": {
Expand Down Expand Up @@ -38,12 +38,11 @@
},
"dependencies": {
"@ai-sdk/openai": "^2.0.64",
"@amplitude/node": "^1.10.0",
"@graphql-tools/merge": "^8.3.1",
"@graphql-tools/schema": "^8.5.1",
"@graphql-tools/utils": "^8.9.0",
"@hawk.so/nodejs": "^3.1.1",
"@hawk.so/types": "^0.5.6",
"@hawk.so/nodejs": "^3.3.1",
"@hawk.so/types": "^0.5.8",
"@n1ru4l/json-patch-plus": "^0.2.0",
"@node-saml/node-saml": "^5.0.1",
"@octokit/oauth-methods": "^4.0.0",
Expand Down
8 changes: 4 additions & 4 deletions src/integrations/github/routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -522,15 +522,15 @@ export function createGitHubRouter(factories: ContextFactories): express.Router
* just log query parameters and respond with 200 without signature validation.
*/
if (req.method !== 'POST') {
// eslint-disable-next-line @typescript-eslint/camelcase, camelcase
const { code, installation_id, setup_action, state, ...restQuery } = req.query as Record<string, unknown>;

// eslint-disable-next-line @typescript-eslint/camelcase, camelcase
if (code || installation_id || state || setup_action) {
// eslint-disable-next-line @typescript-eslint/camelcase, camelcase
log('info', `${WEBHOOK_LOG_PREFIX}Received non-POST request on /webhook with OAuth-like params`, {
// eslint-disable-next-line @typescript-eslint/camelcase, camelcase
code,
installation_id,
setup_action,
installation_id, // eslint-disable-line @typescript-eslint/camelcase, camelcase
setup_action, // eslint-disable-line @typescript-eslint/camelcase, camelcase
state,
query: restQuery,
});
Expand Down
24 changes: 20 additions & 4 deletions src/metrics/graphql.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import client from 'prom-client';
import { ApolloServerPlugin, GraphQLRequestContext, GraphQLRequestListener } from 'apollo-server-plugin-base';
import { GraphQLError } from 'graphql';

import HawkCatcher from '@hawk.so/nodejs';
/**
* GraphQL operation duration histogram
* Tracks GraphQL operation duration by operation name and type
Expand Down Expand Up @@ -71,15 +71,31 @@ export const graphqlMetricsPlugin: ApolloServerPlugin = {
},

async willSendResponse(ctx: GraphQLRequestContext): Promise<void> {
const duration = (Date.now() - startTime) / 1000;
const durationMs = Date.now() - startTime;
const duration = durationMs / 1000;

gqlOperationDuration
.labels(operationName, operationType)
.observe(duration);

const hasErrors = ctx.errors && ctx.errors.length > 0;

HawkCatcher.breadcrumbs.add({
type: 'request',
category: 'GraphQL Operation',
message: `${operationType} ${operationName} ${durationMs}ms${hasErrors ? ` [${ctx.errors!.length} error(s)]` : ''}`,
level: hasErrors ? 'error' : 'debug',
data: {
operationName: { value: operationName },
operationType: { value: operationType },
durationMs: { value: durationMs },
...(hasErrors && { errors: { value: ctx.errors!.map((e: GraphQLError) => e.message).join('; ') } }),
},
});

// Track errors if any
if (ctx.errors && ctx.errors.length > 0) {
ctx.errors.forEach((error: GraphQLError) => {
if (hasErrors) {
ctx.errors!.forEach((error: GraphQLError) => {
const errorType = error.extensions?.code || error.name || 'unknown';

gqlOperationErrors
Expand Down
30 changes: 30 additions & 0 deletions src/metrics/mongodb.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import promClient from 'prom-client';
import { MongoClient, MongoClientOptions } from 'mongodb';
import { Effect, sgr } from '../utils/ansi';
import HawkCatcher from '@hawk.so/nodejs';

/**
* MongoDB command duration histogram
Expand Down Expand Up @@ -306,6 +307,19 @@ export function setupMongoMetrics(client: MongoClient): void {
.labels(metadata.commandName, metadata.collectionFamily, metadata.db)
.observe(duration);

HawkCatcher.breadcrumbs.add({
type: 'request',
category: 'MongoDB Operation',
message: `${metadata.db}.${metadata.collectionFamily}.${metadata.commandName} ${event.duration}ms`,
level: 'debug',
data: {
db: metadata.db,
collection: metadata.collectionFamily,
command: metadata.commandName,
durationMs: { value: event.duration },
},
});

// Clean up metadata
// eslint-disable-next-line @typescript-eslint/no-explicit-any
delete (client as any)[metadataKey];
Expand Down Expand Up @@ -337,6 +351,22 @@ export function setupMongoMetrics(client: MongoClient): void {
.labels(metadata.commandName, errorCode)
.inc();

const errorMsg = (event.failure as any)?.message || 'Unknown error';

HawkCatcher.breadcrumbs.add({
type: 'error',
category: 'MongoDB operation',
message: `${metadata.db}.${metadata.collectionFamily}.${metadata.commandName} FAILED: ${errorMsg} ${event.duration}ms`,
level: 'error',
data: {
db: metadata.db,
collection: metadata.collectionFamily,
command: metadata.commandName,
durationMs: { value: event.duration },
errorCode,
},
});

// Clean up metadata
// eslint-disable-next-line @typescript-eslint/no-explicit-any
delete (client as any)[metadataKey];
Expand Down
9 changes: 0 additions & 9 deletions src/models/usersFactory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ import UserModel from './user';
import { Collection, Db, OptionalId } from 'mongodb';
import DataLoaders from '../dataLoaders';
import { UserDBScheme } from '@hawk.so/types';
import { Analytics, AnalyticsEventTypes } from '../utils/analytics';

/**
* Users factory to work with User Model
Expand Down Expand Up @@ -91,14 +90,6 @@ export default class UsersFactory extends AbstractModelFactory<Omit<UserDBScheme

user.generatedPassword = generatedPassword;

await Analytics.logEvent({
/* eslint-disable-next-line camelcase */
event_type: AnalyticsEventTypes.NEW_USER_REGISTERED,
/* eslint-disable-next-line camelcase */
user_id: userId.toString(),
time: Date.now(),
});

return user;
}

Expand Down
9 changes: 0 additions & 9 deletions src/models/workspacesFactory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import UserModel from './user';
import PlansFactory from './plansFactory';
import PlanModel from './plan';
import { WorkspaceDBScheme } from '@hawk.so/types';
import { Analytics, AnalyticsEventTypes } from '../utils/analytics';

/**
* Workspaces factory to work with WorkspaceModel
Expand Down Expand Up @@ -70,14 +69,6 @@ export default class WorkspacesFactory extends AbstractModelFactory<WorkspaceDBS
await ownerModel.addWorkspace(workspaceModel._id.toString());
await workspaceModel.changePlan((await this.getDefaultPlan())._id.toString());

await Analytics.logEvent({
/* eslint-disable-next-line camelcase */
event_type: AnalyticsEventTypes.WORKSPACE_CREATED,
/* eslint-disable-next-line camelcase */
user_id: ownerModel._id.toString(),
time: Date.now(),
});

return workspaceModel;
}

Expand Down
20 changes: 20 additions & 0 deletions src/rabbitmq.ts
Original file line number Diff line number Diff line change
Expand Up @@ -133,8 +133,28 @@ export async function setupConnections(): Promise<void> {
export async function publish(exchange: string, route: string, message: string, options?: Options.Publish): Promise<void> {
try {
await channel.publish(exchange, route, Buffer.from(message), options);
HawkCatcher.breadcrumbs.add({
type: 'request',
category: 'RabbitMQ Operation',
message: `AMQP publish ${exchange || '(default)'}/${route}`,
level: 'debug',
data: {
exchange: { value: exchange },
route: { value: route },
},
});
debug(`Message sent: ${message}`);
} catch (err) {
HawkCatcher.breadcrumbs.add({
type: 'error',
category: 'RabbitMQ Operation',
message: `AMQP publish FAILED ${exchange || '(default)'}/${route}: ${(err as Error).message}`,
level: 'error',
data: {
exchange: { value: exchange },
route: { value: route },
},
});
HawkCatcher.send(err as Error);
console.log('Message was rejected:', (err as Error).stack);
}
Expand Down
2 changes: 1 addition & 1 deletion src/resolvers/project.js
Original file line number Diff line number Diff line change
Expand Up @@ -421,7 +421,7 @@ module.exports = {
*/
const taskManager = project.taskManager;

if (taskManager && taskManager.type === 'github' && taskManager.config?.installationId) {
if (taskManager && taskManager.type === 'github' && taskManager.config && taskManager.config.installationId) {
const githubService = new GitHubService();

await githubService.deleteInstallation(taskManager.config.installationId);
Expand Down
35 changes: 8 additions & 27 deletions src/typeDefs/event.ts
Original file line number Diff line number Diff line change
Expand Up @@ -116,29 +116,6 @@ type EventUser {
photo: String
}

"""
Breadcrumb severity level
"""
enum BreadcrumbLevel {
fatal
error
warning
info
debug
}

"""
Breadcrumb type - controls categorization and UI appearance
"""
enum BreadcrumbType {
default
request
ui
navigation
logic
error
}

"""
Single breadcrumb entry - represents an event that occurred before the error
"""
Expand All @@ -149,9 +126,11 @@ type Breadcrumb {
timestamp: Float!

"""
Type of breadcrumb - controls UI categorization
Type of breadcrumb - controls UI categorization.
Common values: default, request, ui, navigation, logic, error.
Accepts any string since SDK users may send custom types.
"""
type: BreadcrumbType
type: String

"""
Category of the event - more specific than type
Expand All @@ -164,9 +143,11 @@ type Breadcrumb {
message: String

"""
Severity level of the breadcrumb
Severity level of the breadcrumb.
Common values: fatal, error, warning, info, debug.
Accepts any string since SDK users may send custom levels.
"""
level: BreadcrumbLevel
level: String

"""
Arbitrary key-value data associated with the breadcrumb
Expand Down
27 changes: 0 additions & 27 deletions src/utils/analytics/amplitude.ts

This file was deleted.

17 changes: 0 additions & 17 deletions src/utils/analytics/events.ts

This file was deleted.

2 changes: 0 additions & 2 deletions src/utils/analytics/index.ts

This file was deleted.

5 changes: 5 additions & 0 deletions test/__mocks__/github-service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export const deleteInstallationMock = jest.fn().mockResolvedValue(undefined);

export const GitHubService = jest.fn().mockImplementation(() => ({
deleteInstallation: deleteInstallationMock,
}));
6 changes: 3 additions & 3 deletions test/integrations/github-routes.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -480,7 +480,7 @@ describe('GitHub Routes - /integration/github/connect', () => {

expect(response.status).toBe(302);
expect(response.body).toContain('http://localhost:8080/');
expect(response.body).toContain('error=Missing+or+invalid+OAuth+code');
expect(response.body).toContain('apiError=Missing+or+invalid+OAuth+code');
});

it('should redirect with error when state is missing', async () => {
Expand All @@ -501,7 +501,7 @@ describe('GitHub Routes - /integration/github/connect', () => {

expect(response.status).toBe(302);
expect(response.body).toContain('http://localhost:8080/');
expect(response.body).toContain('error=Missing+or+invalid+state');
expect(response.body).toContain('apiError=Missing+or+invalid+state');
});

it('should redirect with error when state is invalid or expired', async () => {
Expand All @@ -525,7 +525,7 @@ describe('GitHub Routes - /integration/github/connect', () => {

expect(response.status).toBe(302);
expect(response.body).toContain('http://localhost:8080/');
expect(response.body).toContain('error=Invalid+or+expired+state');
expect(response.body).toContain('apiError=Invalid+or+expired+state');
expect(mockGetState).toHaveBeenCalledWith(state);
});

Expand Down
Loading
Loading