Skip to content

Conversation

rclmenezes
Copy link

Rough draft implementation of a fix for #1618 (comment)

@koskimas
Copy link
Collaborator

Sorry, I cant merge this. The query method is a nasty hack. I can merge the change you suggested in the db-errors library as I mentioned in the issue.

@koskimas koskimas closed this Dec 18, 2019
@coveralls
Copy link

Coverage Status

Coverage decreased (-0.03%) to 97.118% when pulling bc00e1d on rclmenezes:support-async-stack-traces into 34910be on Vincit:master.

@rclmenezes
Copy link
Author

rclmenezes commented Dec 18, 2019

It's true it's a hack, so I completely understand :)

That said, you can't fix this issue within db-errors. Custom promise implementations simply won't get the stack trace when await-ed, as I mention here.

Here's a monkey patch if anyone is interested:

import Knex from "knex";
import { Model, Transaction, QueryBuilder } from "objection";

class QueryBuilderWAsyncTrace<A extends Model, B, C> extends QueryBuilder<A, B, C> {
  _asyncStack: any;

  async execute() {
    let p;
    try {
      p = await super.execute();
    } catch (err) {
      if (this._asyncStack) {
        const originalStack = err.stack.split("\n");
        const newStack = originalStack.concat(this._asyncStack);
        err.stack = newStack.join("\n");
      }
      throw err;
    }
    return p;
  }
}

const ModelWithAsyncTrace = <M extends typeof Model>(ModelClass: M): M => {
  return class extends Model {
    static query<T extends Model>(trx?: Transaction | Knex<any, any[]>) {
      const query = (QueryBuilderWAsyncTrace.forClass(this as any).transacting(
        trx as any,
      ) as unknown) as QueryBuilderWAsyncTrace<T, T[], T[]>;

      const knex = this.knex();
      if (knex?.client?.config?.asyncStackTraces) {
        const err = new Error();
        if (err.stack) {
          const lines = err.stack.split("\n").slice(1);
          query._asyncStack = lines;
        }
      }
      (this as any).onCreateQuery(query);
      return query;
    }
  } as any; // Not gonna bother trying to type this one... trust me.
};

export default ModelWithAsyncTrace;

To use, simply extend ModelWithAsyncTrace(Model) instead of Model. E.g.:

class User extends ModelWithAsyncTrace(Model) {
   ...

@koskimas
Copy link
Collaborator

So you are still using objection 1.x? Have you tried 2.0? It uses native promises, which have async stack traces out of the box. There's still some old-fashioned promise code in the codebase which could cut the stack trace, but those could be fixed bu migrating that code to use async/await.

@rclmenezes
Copy link
Author

rclmenezes commented Dec 19, 2019

Yup, I tried 2.0 and it didn't help.

I think any custom promise implementation won't get the full stack trace.

For example, QueryBuilder looks like:

class CustomPromise {
  async execute() {
    throw new Error('Uh-oh');
  }

  then(...args) {
    return this.execute().then(...args);
  }

  catch(...args) {
    return this.execute().catch(...args);
  }
}

async function main() {
  await new CustomPromise();
}

main().catch(e => {
  console.log(e);
});

This code will output:

Error: Uh-oh
    at CustomPromise.execute (/Users/me/code/objection.js/testStack.js:45:11)
    at CustomPromise.then (/Users/me/code/objection.js/testStack.js:49:17)

Note that main is not mentioned at all. Therefore, something that awaits QueryBuilder won't give you a stack trace :(

This is why Knex follows the stack-copying strategy: knex/knex#2500

@capaj
Copy link
Contributor

capaj commented Mar 6, 2020

@rclmenezes thanks for that monkey patch. I was maintaining my own fork of objection.js for the very same reason. Do you know if it works well with objection 2.0 ?

@capaj
Copy link
Contributor

capaj commented Mar 6, 2020

@koskimas maybe then objection.js should consider for the future majors not mimicking the promise API in the QueryBuilder and always require the explicit execute() ?

Or what other solution do you see to get the correct stack traces?

@capaj
Copy link
Contributor

capaj commented Mar 6, 2020

so I tested it now and works like a charm. Stack without it:

 Stack:  [
  'DBError: select (select AVG(results_count) as avg_results from "search_statistics" as "inner_search_statistics" where inner_search_statistics.search_term = search_statistics.search_term) as "avg_results", (select MAX(created_at) as last_searched from "search_statistics" as "inner_search_statistics" where inner_search_statistics.search_term = search_statistics.search_term) as "last_searched", "search_statistics"."search_term", count("search_term") from "search_statistics" where "search_statistics"."organisation_id" = $1 and "created_at" between $2 and $3 and ([object QueryBuilder]) = 0 group by "search_term", "search_statistics"."organisation_id" order by "count" DESC NULLS LAST, lower(search_term) ASC limit $4 - syntax error at or near "["',
  '    at wrapError (/home/capaj/work-repos/looop/project-alpha/node_modules/db-errors/lib/dbErrors.js:19:14)',
  '    at handleExecuteError (/home/capaj/work-repos/looop/project-alpha/node_modules/objection/lib/queryBuilder/QueryBuilder.js:1494:32)',
  '    at LooopQueryBuilder.execute (/home/capaj/work-repos/looop/project-alpha/node_modules/objection/lib/queryBuilder/QueryBuilder.js:685:13)',
  '    at processTicksAndRejections (internal/process/task_queues.js:94:5)'
] +0ms

same error with the monkey patch:

 Stack:  [
  'DBError: select (select AVG(results_count) as avg_results from "search_statistics" as "inner_search_statistics" where inner_search_statistics.search_term = search_statistics.search_term) as "avg_results", (select MAX(created_at) as last_searched from "search_statistics" as "inner_search_statistics" where inner_search_statistics.search_term = search_statistics.search_term) as "last_searched", "search_statistics"."search_term", count("search_term") from "search_statistics" where "search_statistics"."organisation_id" = $1 and "created_at" between $2 and $3 and ([object QueryBuilder]) = 0 group by "search_term", "search_statistics"."organisation_id" order by "count" DESC NULLS LAST, lower(search_term) ASC limit $4 - syntax error at or near "["',
  '    at wrapError (/home/capaj/work-repos/looop/project-alpha/node_modules/db-errors/lib/dbErrors.js:19:14)',
  '    at handleExecuteError (/home/capaj/work-repos/looop/project-alpha/node_modules/objection/lib/queryBuilder/QueryBuilder.js:1494:32)',
  '    at LooopQueryBuilder.execute (/home/capaj/work-repos/looop/project-alpha/node_modules/objection/lib/queryBuilder/QueryBuilder.js:685:13)',
  '    at processTicksAndRejections (internal/process/task_queues.js:94:5)',
  '    at Function.query (/home/capaj/work-repos/looop/project-alpha/back-end/src/models/base/ModelWithAsyncTrace.ts:14:19)',
  '    at AnalyticsSearchModel.createSearchQuery (/home/capaj/work-repos/looop/project-alpha/back-end/src/schemas/models/AnalyticsSearchModel.ts:43:40)',
  '    at AnalyticsSearchModel.searchOverview (/home/capaj/work-repos/looop/project-alpha/back-end/src/schemas/models/AnalyticsSearchModel.ts:82:24)',
  '    at /home/capaj/work-repos/looop/project-alpha/node_modules/decapi/src/domains/field/compiler/resolver.ts:229:45',
  '    at processTicksAndRejections (internal/process/task_queues.js:94:5)'
] +0ms

You just made my day @rclmenezes. Finally I can deprecate my fork.

@lexa0303
Copy link

lexa0303 commented Jun 2, 2020

Are we ever getting this?

@jtomaszewski
Copy link

This is still needed! Even with newest knex, objection, node v15, async stack traces aren't visible. Well explained in #1621 (comment) and also that workaround works. Just remember to also pass asyncStackTraces: true to your knex constructor - without it, the workaround won't work.

@jtomaszewski
Copy link

jtomaszewski commented Mar 11, 2021

I improved a little bit the monkey patch of @rclmenezes to make it work in TS with newest objection and also with other mixins, if e.g. you're using objection-cursor, they'll now work together:

import type Knex from "knex";
import type { Model, Transaction } from "objection";

// Needed to make async error stack traces work
// https://github.com/Vincit/objection.js/pull/1621#issuecomment-567220373
export const ModelWithAsyncStackTrace = <TModelClass extends typeof Model>(
  ModelClass: TModelClass
): TModelClass => {
  class QueryBuilderWithAsyncStackTrace<
    TModel extends Model,
    R = TModel[]
  > extends ModelClass.QueryBuilder<TModel, R> {
    _asyncStack: any;

    async execute() {
      let p;
      try {
        p = await super.execute();
      } catch (error) {
        if (this._asyncStack) {
          const originalStack = error.stack.split("\n");
          const newStack = originalStack.concat(this._asyncStack);
          error.stack = newStack.join("\n");
        }
        throw error;
      }
      return p;
    }
  }

  return class ModelWithAsyncStackTrace extends (ModelClass as any) {
    static get QueryBuilder() {
      return QueryBuilderWithAsyncStackTrace;
    }

    static query<T extends Model>(trx?: Transaction | Knex<any, any[]>) {
      const query = (this.QueryBuilder.forClass(this as any).transacting(
        trx as any
      ) as unknown) as QueryBuilderWithAsyncStackTrace<T, T[]>;

      const knex = this.knex();
      if (knex?.client?.config?.asyncStackTraces) {
        const err = new Error();
        if (err.stack) {
          const lines = err.stack.split("\n").slice(2);
          query._asyncStack = lines;
        }
      }
      (this as any).onCreateQuery(query);
      return query;
    }
  } as any; // Not gonna bother trying to type this one... trust me.
};


// Usage
export abstract class BaseModel extends ModelWithAsyncStackTrace(cursor(Model)) {
}

@samuelsimoes
Copy link

Objection is a wonderful project, but without the proper stack trace, it makes super trick to debug. :(

@rcrogers
Copy link
Contributor

As someone dealing with this frequently, I'd be very excited to see some openness toward fixing this!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

8 participants