Repository: wix/Detox
Pull request #3559 (2022-08-16 14:41:47)

feat(timeline artifact): add invocation trace events.
Add invocation (actions and expectations) started & ended events, along with their stack trace.

As addressed in #3536.

asafkorem: @jonathanmos I made some changes, most of them as agreed f2f, please review 🙏🏼 🙂 

Patch:
diff --git a/detox/src/DetoxExportWrapper.js b/detox/src/DetoxExportWrapper.js
index 99c0c18c1..b80d05645 100644
--- a/detox/src/DetoxExportWrapper.js
+++ b/detox/src/DetoxExportWrapper.js
@@ -66,7 +66,7 @@ class DetoxExportWrapper {
       }
 
       this[_detox] = new Detox(resolvedConfig);
-      await traceCall('detoxInit', () => this[_detox].init());
+      await traceCall('detoxInit', this[_detox].init());
       Detox.none.setError(null);
 
       return this[_detox];
diff --git a/detox/src/android/core/NativeElement.js b/detox/src/android/core/NativeElement.js
index 460a07c81..aa75bcfb8 100644
--- a/detox/src/android/core/NativeElement.js
+++ b/detox/src/android/core/NativeElement.js
@@ -5,6 +5,7 @@ const tempfile = require('tempfile');
 
 const DetoxRuntimeError = require('../../errors/DetoxRuntimeError');
 const invoke = require('../../invoke');
+const { actionDescription } = require('../../utils/invocationTraceDescriptions');
 const actions = require('../actions/native');
 const DetoxMatcherApi = require('../espressoapi/DetoxMatcher');
 const { ActionInteraction } = require('../interactions/native');
@@ -18,12 +19,11 @@ class NativeElement {
   }
 
   _selectElementWithMatcher(matcher) {
-    // if (!(matcher instanceof NativeMatcher)) throw new DetoxRuntimeError(`Element _selectElementWithMatcher argument must be a valid NativeMatcher, got ${typeof matcher}`);
     this._call = invoke.call(invoke.Espresso, 'onView', matcher._call);
   }
 
   atIndex(index) {
-    if (typeof index !== 'number') throw new DetoxRuntimeError(`Element atIndex argument must be a number, got ${typeof index}`);
+    if (typeof index !== 'number') throw new DetoxRuntimeError({ message: `Element atIndex argument must be a number, got ${typeof index}` });
     const matcher = this._originalMatcher;
     this._originalMatcher._call = invoke.callDirectly(DetoxMatcherApi.matcherForAtIndex(index, matcher._call.value));
 
@@ -32,56 +32,84 @@ class NativeElement {
   }
 
   async tap(value) {
-    return await new ActionInteraction(this._invocationManager, this, new actions.TapAction(value)).execute();
+    const action = new actions.TapAction(value);
+    const traceDescription = actionDescription.tapAtPoint(value);
+    return await new ActionInteraction(this._invocationManager, this, action, traceDescription).execute();
   }
 
   async tapAtPoint(value) {
-    return await new ActionInteraction(this._invocationManager, this, new actions.TapAtPointAction(value)).execute();
+    const action = new actions.TapAtPointAction(value);
+    const traceDescription = actionDescription.tapAtPoint(value);
+    return await new ActionInteraction(this._invocationManager, this, action, traceDescription).execute();
   }
 
   async longPress() {
-    return await new ActionInteraction(this._invocationManager, this, new actions.LongPressAction()).execute();
+    const action = new actions.LongPressAction();
+    const traceDescription = actionDescription.longPress();
+    return await new ActionInteraction(this._invocationManager, this, action, traceDescription).execute();
   }
 
   async multiTap(times) {
-    return await new ActionInteraction(this._invocationManager, this, new actions.MultiClickAction(times)).execute();
+    if (typeof times !== 'number') throw new Error('times should be a number, but got ' + (times + (' (' + (typeof times + ')'))));
+    if (times < 1) throw new Error('times should be greater than 0, but got ' + times);
+
+    const action = new actions.MultiClickAction(times);
+    const traceDescription = actionDescription.multiTap(times);
+    return await new ActionInteraction(this._invocationManager, this, action, traceDescription).execute();
   }
 
   async tapBackspaceKey() {
-    return await new ActionInteraction(this._invocationManager, this, new actions.PressKeyAction(67)).execute();
+    const action = new actions.PressKeyAction(67);
+    const traceDescription = actionDescription.tapBackspaceKey();
+    return await new ActionInteraction(this._invocationManager, this, action, traceDescription).execute();
   }
 
   async tapReturnKey() {
-    return await new ActionInteraction(this._invocationManager, this, new actions.TypeTextAction('\n')).execute();
+    const action = new actions.TypeTextAction('\n');
+    const traceDescription = actionDescription.tapReturnKey();
+    return await new ActionInteraction(this._invocationManager, this, action, traceDescription).execute();
   }
 
   async typeText(value) {
-    return await new ActionInteraction(this._invocationManager, this, new actions.TypeTextAction(value)).execute();
+    const action = new actions.TypeTextAction(value);
+    const traceDescription = actionDescription.typeText(value);
+    return await new ActionInteraction(this._invocationManager, this, action, traceDescription).execute();
   }
 
   async replaceText(value) {
-    return await new ActionInteraction(this._invocationManager, this, new actions.ReplaceTextAction(value)).execute();
+    const action = new actions.ReplaceTextAction(value);
+    const traceDescription = actionDescription.replaceText(value);
+    return await new ActionInteraction(this._invocationManager, this, action, traceDescription).execute();
   }
 
   async clearText() {
-    return await new ActionInteraction(this._invocationManager, this, new actions.ClearTextAction()).execute();
+    const action = new actions.ClearTextAction();
+    const traceDescription = actionDescription.clearText();
+    return await new ActionInteraction(this._invocationManager, this, action, traceDescription).execute();
   }
 
   async scroll(amount, direction = 'down', startPositionX, startPositionY) {
-    // override the user's element selection with an extended matcher that looks for UIScrollView children
-    // this._selectElementWithMatcher(this._originalMatcher._extendToDescendantScrollViews());
-    return await new ActionInteraction(this._invocationManager, this, new actions.ScrollAmountAction(direction, amount, startPositionX, startPositionY)).execute();
+    const action = new actions.ScrollAmountAction(direction, amount, startPositionX, startPositionY);
+    const traceDescription = actionDescription.scroll(amount, direction, startPositionX, startPositionY);
+    return await new ActionInteraction(this._invocationManager, this, action, traceDescription).execute();
   }
 
   async scrollTo(edge) {
     // override the user's element selection with an extended matcher that looks for UIScrollView children
     this._selectElementWithMatcher(this._originalMatcher._extendToDescendantScrollViews());
-    return await new ActionInteraction(this._invocationManager, this, new actions.ScrollEdgeAction(edge)).execute();
+
+    const action = new actions.ScrollEdgeAction(edge);
+    const traceDescription = actionDescription.scrollTo(edge);
+    return await new ActionInteraction(this._invocationManager, this, action, traceDescription).execute();
   }
 
   async scrollToIndex(index) {
+    // override the user's element selection with an extended matcher that looks for UIScrollView children
     this._selectElementWithMatcher(this._originalMatcher._extendToDescendantScrollViews());
-    return await new ActionInteraction(this._invocationManager, this, new actions.ScrollToIndex(index)).execute();
+
+    const action = new actions.ScrollToIndex(index);
+    const traceDescription = actionDescription.scrollToIndex(index);
+    return await new ActionInteraction(this._invocationManager, this, action, traceDescription).execute();
   }
 
   /**
@@ -96,13 +124,17 @@ class NativeElement {
 
     // override the user's element selection with an extended matcher that avoids RN issues with RCTScrollView
     this._selectElementWithMatcher(this._originalMatcher._avoidProblematicReactNativeElements());
+
     const action = new actions.SwipeAction(direction, speed, normalizedSwipeOffset, normalizedStartingPointX, normalizedStartingPointY);
-    return await new ActionInteraction(this._invocationManager, this, action).execute();
+    const traceDescription = actionDescription.swipe(direction, speed, normalizedSwipeOffset, normalizedStartingPointX, normalizedStartingPointY);
+    return await new ActionInteraction(this._invocationManager, this, action, traceDescription).execute();
   }
 
   async takeScreenshot(screenshotName) {
     // TODO this should be moved to a lower-layer handler of this use-case
-    const resultBase64 = await new ActionInteraction(this._invocationManager, this, new actions.TakeElementScreenshot()).execute();
+    const action = new actions.TakeElementScreenshot();
+    const traceDescription = actionDescription.takeScreenshot(screenshotName);
+    const resultBase64 = await new ActionInteraction(this._invocationManager, this, action, traceDescription).execute();
     const filePath = tempfile('detox.element-screenshot.png');
     await fs.writeFile(filePath, resultBase64, 'base64');
 
@@ -115,12 +147,16 @@ class NativeElement {
   }
 
   async getAttributes() {
-    const result = await new ActionInteraction(this._invocationManager, this, new actions.GetAttributes()).execute();
+    const action = new actions.GetAttributes();
+    const traceDescription = actionDescription.getAttributes();
+    const result = await new ActionInteraction(this._invocationManager, this, action, traceDescription).execute();
     return JSON.parse(result);
   }
 
   async adjustSliderToPosition(newPosition) {
-    return await new ActionInteraction(this._invocationManager, this, new actions.AdjustSliderToPosition(newPosition)).execute();
+    const action = new actions.AdjustSliderToPosition(newPosition);
+    const traceDescription = actionDescription.adjustSliderToPosition(newPosition);
+    return await new ActionInteraction(this._invocationManager, this, action, traceDescription).execute();
   }
 }
 
diff --git a/detox/src/android/core/NativeExpect.js b/detox/src/android/core/NativeExpect.js
index 548cd920c..e57fe599f 100644
--- a/detox/src/android/core/NativeExpect.js
+++ b/detox/src/android/core/NativeExpect.js
@@ -1,3 +1,4 @@
+const { expectDescription } = require('../../utils/invocationTraceDescriptions');
 const { MatcherAssertionInteraction } = require('../interactions/native');
 const matchers = require('../matchers/native');
 
@@ -19,7 +20,9 @@ class NativeExpectElement extends NativeExpect {
   }
 
   async toBeVisible(pct) {
-    return await new MatcherAssertionInteraction(this._invocationManager, this._element, this._notCondition ? new matchers.VisibleMatcher(pct).not : new matchers.VisibleMatcher(pct)).execute();
+    const matcher = new matchers.VisibleMatcher(pct);
+    const traceDescription = expectDescription.toBeVisible(pct);
+    return await new MatcherAssertionInteraction(this._invocationManager, this._element, matcher, this._notCondition, traceDescription).execute();
   }
 
   async toBeNotVisible() {
@@ -27,7 +30,9 @@ class NativeExpectElement extends NativeExpect {
   }
 
   async toExist() {
-    return await new MatcherAssertionInteraction(this._invocationManager, this._element, this._notCondition ? new matchers.ExistsMatcher().not : new matchers.ExistsMatcher()).execute();
+    const matcher = new matchers.ExistsMatcher();
+    const traceDescription = expectDescription.toExist();
+    return await new MatcherAssertionInteraction(this._invocationManager, this._element, matcher, this._notCondition, traceDescription).execute();
   }
 
   async toNotExist() {
@@ -35,7 +40,9 @@ class NativeExpectElement extends NativeExpect {
   }
 
   async toHaveText(text) {
-    return await new MatcherAssertionInteraction(this._invocationManager, this._element, this._notCondition ? new matchers.TextMatcher(text).not : new matchers.TextMatcher(text)).execute();
+    const matcher = new matchers.TextMatcher(text);
+    const traceDescription = expectDescription.toHaveText(text);
+    return await new MatcherAssertionInteraction(this._invocationManager, this._element, matcher, this._notCondition, traceDescription).execute();
   }
 
   async toNotHaveText(text) {
@@ -43,7 +50,9 @@ class NativeExpectElement extends NativeExpect {
   }
 
   async toHaveLabel(value) {
-    return await new MatcherAssertionInteraction(this._invocationManager, this._element, this._notCondition ? new matchers.LabelMatcher(value).not : new matchers.LabelMatcher(value)).execute();
+    const matcher = new matchers.LabelMatcher(value);
+    const traceDescription = expectDescription.toHaveLabel(value);
+    return await new MatcherAssertionInteraction(this._invocationManager, this._element, matcher, this._notCondition, traceDescription).execute();
   }
 
   async toNotHaveLabel(value) {
@@ -51,7 +60,9 @@ class NativeExpectElement extends NativeExpect {
   }
 
   async toHaveId(value) {
-    return await new MatcherAssertionInteraction(this._invocationManager, this._element, this._notCondition ? new matchers.IdMatcher(value).not : new matchers.IdMatcher(value)).execute();
+    const matcher = new matchers.IdMatcher(value);
+    const traceDescription = expectDescription.toHaveId(value);
+    return await new MatcherAssertionInteraction(this._invocationManager, this._element, matcher, this._notCondition, traceDescription).execute();
   }
 
   async toNotHaveId(value) {
@@ -59,7 +70,9 @@ class NativeExpectElement extends NativeExpect {
   }
 
   async toHaveValue(value) {
-    return await new MatcherAssertionInteraction(this._invocationManager, this._element, this._notCondition ? new matchers.ValueMatcher(value).not : new matchers.ValueMatcher(value)).execute();
+    const matcher = new matchers.ValueMatcher(value);
+    const traceDescription = expectDescription.toHaveValue(value);
+    return await new MatcherAssertionInteraction(this._invocationManager, this._element, matcher, this._notCondition, traceDescription).execute();
   }
 
   async toNotHaveValue(value) {
@@ -67,15 +80,21 @@ class NativeExpectElement extends NativeExpect {
   }
 
   async toHaveToggleValue(value) {
-    return await new MatcherAssertionInteraction(this._invocationManager, this._element, this._notCondition ? new matchers.ToggleMatcher(value).not : new matchers.ToggleMatcher(value)).execute();
+    const matcher = new matchers.ToggleMatcher(value);
+    const traceDescription = expectDescription.toHaveToggleValue(value);
+    return await new MatcherAssertionInteraction(this._invocationManager, this._element, matcher, this._notCondition, traceDescription).execute();
   }
 
   async toHaveSliderPosition(value, tolerance = 0) {
-    return await new MatcherAssertionInteraction(this._invocationManager, this._element, this._notCondition ? new matchers.SliderPositionMatcher(value, tolerance).not : new matchers.SliderPositionMatcher(value, tolerance)).execute();
+    const matcher = new matchers.SliderPositionMatcher(value, tolerance);
+    const traceDescription = expectDescription.toHaveSliderPosition(value, tolerance);
+    return await new MatcherAssertionInteraction(this._invocationManager, this._element, matcher, this._notCondition, traceDescription).execute();
   }
 
   async toBeFocused() {
-    return await new MatcherAssertionInteraction(this._invocationManager, this._element, this._notCondition ? new matchers.FocusMatcher().not : new matchers.FocusMatcher()).execute();
+    const matcher = new matchers.FocusMatcher();
+    const traceDescription = expectDescription.toBeFocused();
+    return await new MatcherAssertionInteraction(this._invocationManager, this._element, matcher, this._notCondition, traceDescription).execute();
   }
 
   async toBeNotFocused() {
diff --git a/detox/src/android/interactions/native.js b/detox/src/android/interactions/native.js
index 69eda00ac..2c0dd002f 100644
--- a/detox/src/android/interactions/native.js
+++ b/detox/src/android/interactions/native.js
@@ -1,4 +1,6 @@
 const DetoxRuntimeError = require('../../errors/DetoxRuntimeError');
+const { expectDescription, actionDescription } = require('../../utils/invocationTraceDescriptions');
+const { traceInvocationCall } = require('../../utils/trace');
 const { ScrollAmountStopAtEdgeAction } = require('../actions/native');
 const { NativeMatcher } = require('../core/NativeMatcher');
 const DetoxAssertionApi = require('../espressoapi/DetoxAssertion');
@@ -9,45 +11,50 @@ function call(maybeAFunction) {
 }
 
 class Interaction {
-  constructor(invocationManager) {
+  constructor(invocationManager, traceDescription) {
     this._call = undefined;
+    this._traceDescription = traceDescription;
     this._invocationManager = invocationManager;
   }
 
   async execute() {
-    const resultObj = await this._invocationManager.execute(this._call);
-    return resultObj ? resultObj.result : undefined;
+    return traceInvocationCall(this._traceDescription, this._call,
+      this._invocationManager.execute(this._call).then((resultObj) => resultObj ? resultObj.result : undefined));
   }
 }
 
 class ActionInteraction extends Interaction {
-  constructor(invocationManager, element, action) {
-    super(invocationManager);
+  constructor(invocationManager, element, action, traceDescription) {
+    super(invocationManager, traceDescription);
     this._call = EspressoDetoxApi.perform(call(element._call), action._call);
     // TODO: move this.execute() here from the caller
   }
 }
 
 class MatcherAssertionInteraction extends Interaction {
-  constructor(invocationManager, element, matcher) {
-    super(invocationManager);
+  constructor(invocationManager, element, matcher, notCondition, traceDescription) {
+    traceDescription = expectDescription.full(traceDescription, notCondition);
+    super(invocationManager, traceDescription);
+
+    matcher = notCondition ? matcher.not : matcher;
     this._call = DetoxAssertionApi.assertMatcher(call(element._call), matcher._call.value);
     // TODO: move this.execute() here from the caller
   }
 }
 
 class WaitForInteraction extends Interaction {
-  constructor(invocationManager, element, assertionMatcher) {
-    super(invocationManager);
+  constructor(invocationManager, element, assertionMatcher, expectTraceDescription) {
+    super(invocationManager, expectTraceDescription);
     this._element = element;
     this._assertionMatcher = assertionMatcher;
     this._element._selectElementWithMatcher(this._element._originalMatcher);
   }
 
   async withTimeout(timeout) {
-    if (typeof timeout !== 'number') throw new DetoxRuntimeError(`WaitForInteraction withTimeout argument must be a number, got ${typeof timeout}`);
-    if (timeout < 0) throw new DetoxRuntimeError('timeout must be larger than 0');
+    if (typeof timeout !== 'number') throw new DetoxRuntimeError({ message: `WaitForInteraction withTimeout argument must be a number, got ${typeof timeout}` });
+    if (timeout < 0) throw new DetoxRuntimeError({ message: 'timeout must be larger than 0' });
 
+    this._traceDescription = expectDescription.waitForWithTimeout(this._traceDescription, timeout);
     this._call = DetoxAssertionApi.waitForAssertMatcher(call(this._element._call), this._assertionMatcher._call.value, timeout / 1000);
     await this.execute();
   }
@@ -58,20 +65,18 @@ class WaitForInteraction extends Interaction {
 }
 
 class WaitForActionInteractionBase extends Interaction {
-  constructor(invocationManager, element, matcher, searchMatcher) {
-    super(invocationManager);
-    //if (!(element instanceof NativeElement)) throw new DetoxRuntimeError(`WaitForActionInteraction ctor 1st argument must be a valid NativeElement, got ${typeof element}`);
-    //if (!(matcher instanceof NativeMatcher)) throw new DetoxRuntimeError(`WaitForActionInteraction ctor 2nd argument must be a valid NativeMatcher, got ${typeof matcher}`);
+  constructor(invocationManager, element, matcher, searchMatcher, traceDescription) {
+    super(invocationManager, traceDescription);
+
     if (!(searchMatcher instanceof NativeMatcher))
-      throw new DetoxRuntimeError(`WaitForActionInteraction ctor 3rd argument must be a valid NativeMatcher, got ${typeof searchMatcher}`);
+      throw new DetoxRuntimeError({ message: `WaitForActionInteraction ctor 3rd argument must be a valid NativeMatcher, got ${typeof searchMatcher}` });
+
     this._element = element;
     this._originalMatcher = matcher;
     this._searchMatcher = searchMatcher;
   }
 
   _prepare(searchAction) {
-    //if (!searchAction instanceof Action) throw new DetoxRuntimeError(`WaitForActionInteraction _execute argument must be a valid Action, got ${typeof searchAction}`);
-
     this._call = DetoxAssertionApi.waitForAssertMatcherWithSearchAction(
       call(this._element._call),
       call(this._originalMatcher._call).value,
@@ -83,6 +88,7 @@ class WaitForActionInteractionBase extends Interaction {
 
 class WaitForActionInteraction extends WaitForActionInteractionBase {
   async scroll(amount, direction = 'down', scrollPositionX, scrollPositionY) {
+    this._traceDescription = expectDescription.waitFor(actionDescription.scroll(amount, direction, scrollPositionX, scrollPositionY));
     this._prepare(new ScrollAmountStopAtEdgeAction(direction, amount, scrollPositionX, scrollPositionY));
     await this.execute();
   }
diff --git a/detox/src/artifacts/timeline/TimelineArtifactPlugin.js b/detox/src/artifacts/timeline/TimelineArtifactPlugin.js
index 6923701d8..3c62a187a 100644
--- a/detox/src/artifacts/timeline/TimelineArtifactPlugin.js
+++ b/detox/src/artifacts/timeline/TimelineArtifactPlugin.js
@@ -7,16 +7,13 @@ const { trace } = require('../../utils/trace');
 const FileArtifact = require('../templates/artifact/FileArtifact');
 const ArtifactPlugin = require('../templates/plugin/ArtifactPlugin');
 
+const TIMELINE_CONTEXT_TYPES = require('./TimelineContextTypes');
+
 const traceNoop = {
   startSection: _noop,
   endSection: _noop,
 };
 
-const CONTEXT_TYPES = {
-  TEST: 'test',
-  DESCRIBE: 'describe'
-};
-
 class TimelineArtifactPlugin extends ArtifactPlugin {
   constructor(config) {
     super(config);
@@ -37,23 +34,23 @@ class TimelineArtifactPlugin extends ArtifactPlugin {
 
   async onRunDescribeStart(suite) {
     const sectionName = (suite.name === 'ROOT_DESCRIBE_BLOCK' ? this._deviceName : suite.name);
-    this._trace.startSection(sectionName, { context: CONTEXT_TYPES.DESCRIBE });
+    this._trace.startSection(sectionName, { context: TIMELINE_CONTEXT_TYPES.DESCRIBE });
     await super.onRunDescribeStart(suite);
   }
 
   async onRunDescribeFinish(suite) {
     const sectionName = (suite.name === 'ROOT_DESCRIBE_BLOCK' ? this._deviceName : suite.name);
-    this._trace.endSection(sectionName, { context: CONTEXT_TYPES.DESCRIBE });
+    this._trace.endSection(sectionName, { context: TIMELINE_CONTEXT_TYPES.DESCRIBE });
     await super.onRunDescribeFinish(suite);
   }
 
   async onTestStart(testSummary) {
-    this._trace.startSection(testSummary.title, { context: CONTEXT_TYPES.TEST });
+    this._trace.startSection(testSummary.title, { context: TIMELINE_CONTEXT_TYPES.TEST });
     await super.onTestStart(testSummary);
   }
 
   async onTestDone(testSummary) {
-    this._trace.endSection(testSummary.title, { status: testSummary.status, context: CONTEXT_TYPES.TEST });
+    this._trace.endSection(testSummary.title, { status: testSummary.status, context: TIMELINE_CONTEXT_TYPES.TEST });
     await super.onTestDone(testSummary);
   }
 
diff --git a/detox/src/artifacts/timeline/TimelineContextTypes.js b/detox/src/artifacts/timeline/TimelineContextTypes.js
new file mode 100644
index 000000000..b43941847
--- /dev/null
+++ b/detox/src/artifacts/timeline/TimelineContextTypes.js
@@ -0,0 +1,7 @@
+const TIMELINE_CONTEXT_TYPES = {
+  TEST: 'test',
+  DESCRIBE: 'describe',
+  INVOCATION: 'invocation'
+};
+
+module.exports = TIMELINE_CONTEXT_TYPES;
diff --git a/detox/src/devices/allocation/DeviceAllocator.js b/detox/src/devices/allocation/DeviceAllocator.js
index 6a2675a55..86dcea1eb 100644
--- a/detox/src/devices/allocation/DeviceAllocator.js
+++ b/detox/src/devices/allocation/DeviceAllocator.js
@@ -14,8 +14,7 @@ class DeviceAllocator {
    * @return {Promise<DeviceCookie>}
    */
   allocate(deviceConfig) {
-    return traceCall('allocateDevice', () =>
-      this._driver.allocate(deviceConfig));
+    return traceCall('allocateDevice', this._driver.allocate(deviceConfig));
   }
 
   /**
diff --git a/detox/src/devices/allocation/drivers/android/emulator/EmulatorAllocDriver.js b/detox/src/devices/allocation/drivers/android/emulator/EmulatorAllocDriver.js
index c9cf360ce..74272f96e 100644
--- a/detox/src/devices/allocation/drivers/android/emulator/EmulatorAllocDriver.js
+++ b/detox/src/devices/allocation/drivers/android/emulator/EmulatorAllocDriver.js
@@ -73,7 +73,7 @@ class EmulatorAllocDriver extends AllocationDriverBase {
 
   async _launchEmulator(avdName, adbName, isRunning, options) {
     try {
-      await traceCall('emulatorLaunch', () =>
+      await traceCall('emulatorLaunch',
         this._emulatorLauncher.launch(avdName, adbName, isRunning, options));
     } catch (e) {
       await this._allocationHelper.deallocateDevice(adbName);
diff --git a/detox/src/devices/allocation/drivers/android/emulator/EmulatorLauncher.js b/detox/src/devices/allocation/drivers/android/emulator/EmulatorLauncher.js
index 2557c1ae3..3bc3ad87e 100644
--- a/detox/src/devices/allocation/drivers/android/emulator/EmulatorLauncher.js
+++ b/detox/src/devices/allocation/drivers/android/emulator/EmulatorLauncher.js
@@ -63,8 +63,7 @@ class EmulatorLauncher extends DeviceLauncher {
   }
 
   async _awaitEmulatorBoot(adbName) {
-    await traceCall('awaitBoot', () =>
-      this._waitForBootToComplete(adbName));
+    await traceCall('awaitBoot', this._waitForBootToComplete(adbName));
   }
 
   async _waitForBootToComplete(adbName) {
diff --git a/detox/src/devices/runtime/RuntimeDevice.js b/detox/src/devices/runtime/RuntimeDevice.js
index 336b15915..fc18d4d67 100644
--- a/detox/src/devices/runtime/RuntimeDevice.js
+++ b/detox/src/devices/runtime/RuntimeDevice.js
@@ -113,7 +113,7 @@ class RuntimeDevice {
   }
 
   async launchApp(params = {}, bundleId = this._bundleId) {
-    return traceCall('launchApp', () => this._doLaunchApp(params, bundleId));
+    return traceCall('launch app', this._doLaunchApp(params, bundleId));
   }
 
   /**
@@ -179,29 +179,25 @@ class RuntimeDevice {
   }
 
   async installApp(binaryPath, testBinaryPath) {
-    await traceCall('appInstall', () => {
-      const currentApp = binaryPath ? { binaryPath, testBinaryPath } : this._getCurrentApp();
-      return this.deviceDriver.installApp(currentApp.binaryPath, currentApp.testBinaryPath);
-    });
+    const currentApp = binaryPath ? { binaryPath, testBinaryPath } : this._getCurrentApp();
+    await traceCall('appInstall',
+      this.deviceDriver.installApp(currentApp.binaryPath, currentApp.testBinaryPath));
   }
 
   async uninstallApp(bundleId) {
     const _bundleId = bundleId || this._bundleId;
-    await traceCall('appUninstall', () =>
-      this.deviceDriver.uninstallApp(_bundleId));
+    await traceCall('appUninstall', this.deviceDriver.uninstallApp(_bundleId));
   }
 
   async installUtilBinaries() {
     const paths = this._deviceConfig.utilBinaryPaths;
     if (paths) {
-      await traceCall('installUtilBinaries', () =>
-        this.deviceDriver.installUtilBinaries(paths));
+      await traceCall('installUtilBinaries', this.deviceDriver.installUtilBinaries(paths));
     }
   }
 
   async reloadReactNative() {
-    await traceCall('reloadRN', () =>
-      this.deviceDriver.reloadReactNative());
+    await traceCall('reload React Native', this.deviceDriver.reloadReactNative());
   }
 
   async openURL(params) {
diff --git a/detox/src/ios/expectTwo.js b/detox/src/ios/expectTwo.js
index 76743391c..93f7b198d 100644
--- a/detox/src/ios/expectTwo.js
+++ b/detox/src/ios/expectTwo.js
@@ -1,4 +1,5 @@
 // @ts-nocheck
+const assert = require('assert');
 const path = require('path');
 
 const fs = require('fs-extra');
@@ -6,6 +7,8 @@ const _ = require('lodash');
 const tempfile = require('tempfile');
 
 const { assertEnum, assertNormalized } = require('../utils/assertArgument');
+const { actionDescription, expectDescription } = require('../utils/invocationTraceDescriptions');
+const { traceInvocationCall } = require('../utils/trace');
 
 const assertDirection = assertEnum(['left', 'right', 'up', 'down']);
 const assertSpeed = assertEnum(['fast', 'slow']);
@@ -23,7 +26,8 @@ class Expect {
                       + (percent + (' (' + (typeof percent + ')'))));
     }
 
-    return this.expect('toBeVisible', percent);
+    const traceDescription = expectDescription.toBeVisible(percent);
+    return this.expect('toBeVisible', traceDescription, percent);
   }
 
   toBeNotVisible() {
@@ -31,7 +35,8 @@ class Expect {
   }
 
   toBeFocused() {
-    return this.expect('toBeFocused');
+    const traceDescription = expectDescription.toBeFocused();
+    return this.expect('toBeFocused', traceDescription);
   }
 
   toBeNotFocused() {
@@ -39,7 +44,8 @@ class Expect {
   }
 
   toExist() {
-    return this.expect('toExist');
+    const traceDescription = expectDescription.toExist();
+    return this.expect('toExist', traceDescription);
   }
 
   toNotExist() {
@@ -47,7 +53,8 @@ class Expect {
   }
 
   toHaveText(text) {
-    return this.expect('toHaveText', text);
+    const traceDescription = expectDescription.toHaveText(text);
+    return this.expect('toHaveText', traceDescription, text);
   }
 
   toNotHaveText(text) {
@@ -55,7 +62,8 @@ class Expect {
   }
 
   toHaveLabel(label) {
-    return this.expect('toHaveLabel', label);
+    const traceDescription = expectDescription.toHaveLabel(label);
+    return this.expect('toHaveLabel', traceDescription, label);
   }
 
   toNotHaveLabel(label) {
@@ -63,7 +71,8 @@ class Expect {
   }
 
   toHaveId(id) {
-    return this.expect('toHaveId', id);
+    const traceDescription = expectDescription.toHaveId(id);
+    return this.expect('toHaveId', traceDescription, id);
   }
 
   toNotHaveId(id) {
@@ -71,7 +80,8 @@ class Expect {
   }
 
   toHaveValue(value) {
-    return this.expect('toHaveValue', value);
+    const traceDescription = expectDescription.toHaveValue(value);
+    return this.expect('toHaveValue', traceDescription, value);
   }
 
   toNotHaveValue(value) {
@@ -79,7 +89,8 @@ class Expect {
   }
 
   toHaveSliderPosition(position, tolerance = 0) {
-    return this.expect('toHaveSliderPosition', position, tolerance);
+    const traceDescription = expectDescription.toHaveSliderPosition(position, tolerance);
+    return this.expect('toHaveSliderPosition', traceDescription, position, tolerance);
   }
 
   toHaveToggleValue(value) {
@@ -103,16 +114,18 @@ class Expect {
     };
   }
 
-  expect(expectation, ...params) {
+  expect(expectation, traceDescription, ...params) {
+    assert(traceDescription, `must provide trace description for expectation: \n ${JSON.stringify(expectation)}`);
+
     const invocation = this.createInvocation(expectation, ...params);
-    return this._invocationManager.execute(invocation);
+    traceDescription = expectDescription.full(traceDescription, this.modifiers.includes('not'));
+    return _executeInvocation(this._invocationManager, invocation, traceDescription);
   }
 }
 
 class InternalExpect extends Expect {
-  expect(expectation, ...params) {
-    const invocation = this.createInvocation(expectation, ...params);
-    return invocation;
+  expect(expectation, _traceDescription, ...params) {
+    return this.createInvocation(expectation, ...params);
   }
 }
 
@@ -131,7 +144,8 @@ class Element {
   }
 
   getAttributes() {
-    return this.withAction('getAttributes');
+    const traceDescription = actionDescription.getAttributes();
+    return this.withAction('getAttributes', traceDescription);
   }
 
   tap(point) {
@@ -140,12 +154,16 @@ class Element {
       if (typeof point.x !== 'number') throw new Error('point.x should be a number, but got ' + (point.x + (' (' + (typeof point.x + ')'))));
       if (typeof point.y !== 'number') throw new Error('point.y should be a number, but got ' + (point.y + (' (' + (typeof point.y + ')'))));
     }
-    return this.withAction('tap', point);
+
+    const traceDescription = actionDescription.tapAtPoint(point);
+    return this.withAction('tap', traceDescription, point);
   }
 
   longPress(duration = 1000) {
     if (typeof duration !== 'number') throw new Error('duration should be a number, but got ' + (duration + (' (' + (typeof duration + ')'))));
-    return this.withAction('longPress', duration);
+
+    const traceDescription = actionDescription.longPress(duration);
+    return this.withAction('longPress', traceDescription, duration);
   }
 
   longPressAndDrag(duration, normalizedPositionX, normalizedPositionY, targetElement,
@@ -162,13 +180,18 @@ class Element {
     assertNormalized({ normalizedTargetPositionX });
     assertNormalized({ normalizedTargetPositionY });
 
-    return this.withActionAndTargetElement('longPress', targetElement, duration, normalizedPositionX, normalizedPositionY,
+    const traceDescription = actionDescription.longPressAndDrag(duration, normalizedPositionX, normalizedPositionY, targetElement,
+      normalizedTargetPositionX, normalizedTargetPositionY, speed, holdDuration);
+    return this.withActionAndTargetElement('longPress', targetElement, traceDescription, duration, normalizedPositionX, normalizedPositionY,
       normalizedTargetPositionX, normalizedTargetPositionY, speed, holdDuration);
   }
 
   multiTap(times) {
     if (typeof times !== 'number') throw new Error('times should be a number, but got ' + (times + (' (' + (typeof times + ')'))));
-    return this.withAction('multiTap', times);
+    if (times < 1) throw new Error('times should be greater than 0, but got ' + times);
+
+    const traceDescription = actionDescription.multiTap(times);
+    return this.withAction('multiTap', traceDescription, times);
   }
 
   tapAtPoint(point) {
@@ -176,25 +199,32 @@ class Element {
   }
 
   tapBackspaceKey() {
-    return this.withAction('tapBackspaceKey');
+    const traceDescription = actionDescription.tapBackspaceKey();
+    return this.withAction('tapBackspaceKey', traceDescription);
   }
 
   tapReturnKey() {
-    return this.withAction('tapReturnKey');
+    const traceDescription = actionDescription.tapReturnKey();
+    return this.withAction('tapReturnKey', traceDescription);
   }
 
   typeText(text) {
     if (typeof text !== 'string') throw new Error('text should be a string, but got ' + (text + (' (' + (typeof text + ')'))));
-    return this.withAction('typeText', text);
+
+    const traceDescription = actionDescription.typeText(text);
+    return this.withAction('typeText', traceDescription, text);
   }
 
   replaceText(text) {
     if (typeof text !== 'string') throw new Error('text should be a string, but got ' + (text + (' (' + (typeof text + ')'))));
-    return this.withAction('replaceText', text);
+
+    const traceDescription = actionDescription.replaceText(text);
+    return this.withAction('replaceText', traceDescription, text);
   }
 
   clearText() {
-    return this.withAction('clearText');
+    const traceDescription = actionDescription.clearText();
+    return this.withAction('clearText', traceDescription);
   }
 
   scroll(pixels, direction = 'down', startPositionX = NaN, startPositionY = NaN) {
@@ -202,12 +232,15 @@ class Element {
     if (typeof pixels !== 'number') throw new Error('amount of pixels should be a number, but got ' + (pixels + (' (' + (typeof pixels + ')'))));
     if (typeof startPositionX !== 'number') throw new Error('startPositionX should be a number, but got ' + (startPositionX + (' (' + (typeof startPositionX + ')'))));
     if (typeof startPositionY !== 'number') throw new Error('startPositionY should be a number, but got ' + (startPositionY + (' (' + (typeof startPositionY + ')'))));
-    return this.withAction('scroll', pixels, direction, startPositionX, startPositionY);
+
+    const traceDescription = actionDescription.scroll(pixels, direction, startPositionX, startPositionY);
+    return this.withAction('scroll', traceDescription, pixels, direction, startPositionX, startPositionY);
   }
 
   scrollTo(edge) {
     if (!['left', 'right', 'top', 'bottom'].some(option => option === edge)) throw new Error('edge should be one of [left, right, top, bottom], but got ' + edge);
-    return this.withAction('scrollTo', edge);
+    const traceDescription = actionDescription.scrollTo(edge);
+    return this.withAction('scrollTo', traceDescription, edge);
   }
 
   swipe(direction, speed = 'fast', normalizedSwipeOffset = NaN, normalizedStartingPointX = NaN, normalizedStartingPointY = NaN) {
@@ -218,42 +251,62 @@ class Element {
     assertNormalized({ normalizedStartingPointY });
 
     normalizedSwipeOffset = Number.isNaN(normalizedSwipeOffset) ? 0.75 : normalizedSwipeOffset;
-    return this.withAction('swipe', direction, speed, normalizedSwipeOffset, normalizedStartingPointX, normalizedStartingPointY);
+    const traceDescription = actionDescription.swipe(direction, speed, normalizedSwipeOffset, normalizedStartingPointX, normalizedStartingPointY);
+    return this.withAction(
+      'swipe',
+      traceDescription,
+      direction,
+      speed,
+      normalizedSwipeOffset,
+      normalizedStartingPointX,
+      normalizedStartingPointY
+    );
   }
 
   setColumnToValue(column, value) {
     if (typeof column !== 'number') throw new Error('column should be a number, but got ' + (column + (' (' + (typeof column + ')'))));
     if (typeof value !== 'string') throw new Error('value should be a string, but got ' + (value + (' (' + (typeof value + ')'))));
-    return this.withAction('setColumnToValue', column, value);
+
+    const traceDescription = actionDescription.setColumnToValue(column, value);
+    return this.withAction('setColumnToValue', traceDescription, column, value);
   }
 
   setDatePickerDate(dateString, dateFormat) {
     if (typeof dateString !== 'string') throw new Error('dateString should be a string, but got ' + (dateString + (' (' + (typeof dateString + ')'))));
     if (typeof dateFormat !== 'string') throw new Error('dateFormat should be a string, but got ' + (dateFormat + (' (' + (typeof dateFormat + ')'))));
-    return this.withAction('setDatePickerDate', dateString, dateFormat);
+
+    const traceDescription = actionDescription.setDatePickerDate(dateString, dateFormat);
+    return this.withAction('setDatePickerDate', traceDescription, dateString, dateFormat);
   }
 
   pinch(scale, speed = 'fast', angle = 0) {
     if (typeof scale !== 'number' || !Number.isFinite(scale) || scale < 0) throw new Error(`pinch scale must be a finite number larger than zero`);
     if (!['slow', 'fast'].includes(speed)) throw new Error(`pinch speed is either 'slow' or 'fast'`);
     if (typeof angle !== 'number' || !Number.isFinite(angle)) throw new Error(`pinch angle must be a finite number (radian)`);
-    return this.withAction('pinch', scale, speed, angle);
+
+    const traceDescription = actionDescription.pinch(scale, speed, angle);
+    return this.withAction('pinch', traceDescription, scale, speed, angle);
   }
 
   pinchWithAngle(direction, speed = 'slow', angle = 0) {
     if (!['inward', 'outward'].includes(direction)) throw new Error(`pinchWithAngle direction is either 'inward' or 'outward'`);
     if (!['slow', 'fast'].includes(speed)) throw new Error(`pinchWithAngle speed is either 'slow' or 'fast'`);
     if (typeof angle !== 'number') throw new Error(`pinchWithAngle angle must be a number (radiant), got ${typeof angle}`);
-    return this.withAction('pinchWithAngle', direction, speed, angle);
+
+    const traceDescription = actionDescription.pinchWithAngle(direction, speed, angle);
+    return this.withAction('pinchWithAngle', traceDescription, direction, speed, angle);
   }
 
   adjustSliderToPosition(position) {
     if (!(typeof position === 'number' && position >= 0 && position <= 1)) throw new Error('position should be a number [0.0, 1.0], but got ' + (position + (' (' + (typeof position + ')'))));
-    return this.withAction('adjustSliderToPosition', position);
+
+    const traceDescription = actionDescription.adjustSliderToPosition(position);
+    return this.withAction('adjustSliderToPosition', traceDescription, position);
   }
 
   async takeScreenshot(fileName) {
-    const { screenshotPath } = await this.withAction('takeScreenshot', fileName);
+    const traceDescription = actionDescription.takeScreenshot(fileName);
+    const { screenshotPath } = await this.withAction('takeScreenshot', traceDescription, fileName);
 
     const filePath = tempfile('.detox.element-screenshot.png');
     await fs.move(screenshotPath, filePath);
@@ -287,22 +340,20 @@ class Element {
     return invocation;
   }
 
-  withAction(action, ...params) {
+  withAction(action, traceDescription, ...params) {
     const invocation = this.createInvocation(action, null, ...params);
-    return this._invocationManager.execute(invocation);
+    return _executeInvocation(this._invocationManager, invocation, traceDescription);
   }
 
-  withActionAndTargetElement(action, targetElement, ...params) {
+  withActionAndTargetElement(action, targetElement, traceDescription, ...params) {
     const invocation = this.createInvocation(action, targetElement, ...params);
-    return this._invocationManager.execute(invocation);
+    return _executeInvocation(this._invocationManager, invocation, traceDescription);
   }
 }
 
 class InternalElement extends Element {
-
-  withAction(action, ...params) {
-    const invocation = this.createInvocation(action, null, ...params);
-    return invocation;
+  withAction(action, _traceDescription, ...params) {
+    return this.createInvocation(action, null, ...params);
   }
 }
 
@@ -341,7 +392,6 @@ class By {
 }
 
 class Matcher {
-
   accessibilityLabel(label) {
     return this.label(label);
   }
@@ -395,14 +445,12 @@ class Matcher {
     if (!(matcher instanceof Matcher)) {
       throwMatcherError(matcher);
     }
+
     this.and({ predicate: { type: 'descendant', predicate: matcher.predicate } });
     return this;
   }
 
   and(matcher) {
-    // if (!(matcher instanceof Matcher)) {
-    //   throwMatcherError(matcher);
-    // }
     const tempPredicate = this.predicate;
     delete this.predicate;
     this.predicate = { type: 'and', predicates: [] };
@@ -493,7 +541,9 @@ class WaitFor {
     if (typeof timeout !== 'number') throw new Error('text should be a number, but got ' + (timeout + (' (' + (typeof timeout + ')'))));
     if (timeout < 0) throw new Error('timeout must be larger than 0');
     this.timeout = timeout;
-    return this.waitForWithTimeout();
+
+    const traceDescription = expectDescription.withTimeout(timeout);
+    return this.waitForWithTimeout(traceDescription);
   }
 
   whileElement(matcher) {
@@ -504,106 +554,137 @@ class WaitFor {
 
   tap(point) {
     this.action = this.actionableElement.tap(point);
-    return this.waitForWithAction();
+    const traceDescription = actionDescription.tapAtPoint(point);
+    return this.waitForWithAction(traceDescription);
   }
 
   longPress(duration) {
     this.action = this.actionableElement.longPress(duration);
-    return this.waitForWithAction();
+    const traceDescription = actionDescription.longPress(duration);
+    return this.waitForWithAction(traceDescription);
   }
 
   multiTap(times) {
     this.action = this.actionableElement.multiTap(times);
-    return this.waitForWithAction();
+    const traceDescription = actionDescription.multiTap(times);
+    return this.waitForWithAction(traceDescription);
   }
 
   tapAtPoint(point) {
     this.action = this.actionableElement.tap(point);
-    return this.waitForWithAction();
+    const traceDescription = actionDescription.tapAtPoint(point);
+    return this.waitForWithAction(traceDescription);
   }
 
   tapBackspaceKey() {
     this.action = this.actionableElement.tapBackspaceKey();
-    return this.waitForWithAction();
+    const traceDescription = actionDescription.tapBackspaceKey();
+    return this.waitForWithAction(traceDescription);
   }
 
   tapReturnKey() {
     this.action = this.actionableElement.tapReturnKey();
-    return this.waitForWithAction();
+    const traceDescription = actionDescription.tapReturnKey();
+    return this.waitForWithAction(traceDescription);
   }
 
   typeText(text) {
     this.action = this.actionableElement.typeText(text);
-    return this.waitForWithAction();
+    const traceDescription = actionDescription.typeText(text);
+    return this.waitForWithAction(traceDescription);
   }
 
   replaceText(text) {
     this.action = this.actionableElement.replaceText(text);
-    return this.waitForWithAction();
+    const traceDescription = actionDescription.replaceText(text);
+    return this.waitForWithAction(traceDescription);
   }
 
   clearText() {
     this.action = this.actionableElement.clearText();
-    return this.waitForWithAction();
+    const traceDescription = actionDescription.clearText();
+    return this.waitForWithAction(traceDescription);
   }
 
   scroll(pixels, direction, startPositionX, startPositionY) {
     this.action = this.actionableElement.scroll(pixels, direction, startPositionX, startPositionY);
-    return this.waitForWithAction();
+
+    const traceDescription = actionDescription.scroll(pixels, direction, startPositionX, startPositionY);
+    return this.waitForWithAction(traceDescription);
   }
 
   scrollTo(edge) {
     this.action = this.actionableElement.scrollTo(edge);
-    return this.waitForWithAction();
+    const traceDescription = actionDescription.scrollTo(edge);
+    return this.waitForWithAction(traceDescription);
   }
 
   swipe(direction, speed, percentage) {
     this.action = this.actionableElement.swipe(direction, speed, percentage);
-    return this.waitForWithAction();
+    const traceDescription = actionDescription.swipe(direction, speed, percentage);
+    return this.waitForWithAction(traceDescription);
   }
 
   setColumnToValue(column, value) {
     this.action = this.actionableElement.setColumnToValue(column, value);
-    return this.waitForWithAction();
+    const traceDescription = actionDescription.setColumnToValue(column, value);
+    return this.waitForWithAction(traceDescription);
   }
 
   setDatePickerDate(dateString, dateFormat) {
     this.action = this.actionableElement.setDatePickerDate(dateString, dateFormat);
-    return this.waitForWithAction();
+    const traceDescription = actionDescription.setDatePickerDate(dateString, dateFormat);
+    return this.waitForWithAction(traceDescription);
   }
 
   pinch(scale, speed, angle) {
     this.action = this.actionableElement.pinch(scale, speed, angle);
-    return this.waitForWithAction();
+    const traceDescription = actionDescription.pinch(scale, speed, angle);
+    return this.waitForWithAction(traceDescription);
   }
 
   pinchWithAngle(direction, speed, angle) {
     this.action = this.actionableElement.pinchWithAngle(direction, speed, angle);
-    return this.waitForWithAction();
+    const traceDescription = actionDescription.pinchWithAngle(direction, speed, angle);
+    return this.waitForWithAction(traceDescription);
   }
 
-  waitForWithAction() {
+  waitForWithAction(actionTraceDescription) {
     const expectation = this.expectation;
     const action = this.action;
 
-    return this._invocationManager.execute({
+    const invocation = this.createWaitForWithActionInvocation(expectation, action);
+
+    const traceDescription = expectDescription.waitFor(actionTraceDescription);
+    return _executeInvocation(this._invocationManager, invocation, traceDescription);
+  }
+
+  createWaitForWithActionInvocation(expectation, action) {
+    return {
       ...action,
       while: {
         ...expectation
       }
-    });
+    };
   }
 
-  waitForWithTimeout() {
+  waitForWithTimeout(expectTraceDescription) {
     const expectation = this.expectation;
     const action = this.action;
     const timeout = this.timeout;
 
-    return this._invocationManager.execute({
+    const invocation = this.createWaitForWithTimeoutInvocation(expectation, action, timeout);
+
+    const traceDescription = expectDescription.waitForWithTimeout(expectTraceDescription, timeout);
+    return _executeInvocation(this._invocationManager, invocation, traceDescription);
+  }
+
+  createWaitForWithTimeoutInvocation(expectation, action, timeout) {
+    return {
       ...action,
       ...expectation,
       timeout
-    });
+    };
   }
 }
 
@@ -665,4 +746,8 @@ function throwElementError(param) {
   throw new Error(`${param} is not a Detox element. More about Detox elements here: https://wix.github.io/Detox/docs/api/matchers`);
 }
 
+function _executeInvocation(invocationManager, invocation, traceDescription) {
+  return traceInvocationCall(traceDescription, invocation, invocationManager.execute(invocation));
+}
+
 module.exports = IosExpect;
diff --git a/detox/src/utils/__mocks__/trace.js b/detox/src/utils/__mocks__/trace.js
index e69fae1cb..82e9ecb99 100644
--- a/detox/src/utils/__mocks__/trace.js
+++ b/detox/src/utils/__mocks__/trace.js
@@ -6,9 +6,13 @@ class FakeTrace {
     this.events = [];
   }
 }
-const traceCall = jest.fn().mockImplementation((_, fn) => fn());
+
+const traceCall = jest.fn().mockImplementation((__, promise) => promise);
+
+const traceInvocationCall = jest.fn().mockImplementation((__, ___, promise) => promise);
 
 module.exports = {
   trace: new FakeTrace(),
   traceCall,
+  traceInvocationCall,
 };
diff --git a/detox/src/utils/invocationTraceDescriptions.js b/detox/src/utils/invocationTraceDescriptions.js
new file mode 100644
index 000000000..246699623
--- /dev/null
+++ b/detox/src/utils/invocationTraceDescriptions.js
@@ -0,0 +1,43 @@
+module.exports = {
+  actionDescription: {
+    adjustSliderToPosition: (newPosition) => `adjust slider to position ${newPosition}`,
+    clearText: () => 'clear input text',
+    getAttributes: () => 'get element attributes',
+    longPress: (duration) => `long press${duration !== undefined ? ` for ${duration}ms` : ''}`,
+    longPressAndDrag: (duration, startX, startY, targetElement, endX, endY, speed, holdDuration) =>
+      `long press and drag from ${startX}, ${startY} to ${endX}, ${endY} with speed ${speed} and hold duration ${holdDuration}`,
+    multiTap: (times) => `tap ${times} times`,
+    pinch: (scale, speed, angle) => `pinch with scale ${scale}, speed ${speed}, and angle ${angle}`,
+    pinchWithAngle: (direction, speed, angle) => `pinch with direction ${direction}, speed ${speed}, and angle ${angle}`,
+    replaceText: (value) => `replace input text: "${value}"`,
+    scroll: (amount, direction, startPositionX, startPositionY) =>
+      `scroll ${amount} pixels ${direction}${startPositionX !== undefined && startPositionY !== undefined ? ` from normalized position (${startPositionX}, ${startPositionY})` : ''}`,
+    scrollTo: (edge) => `scroll to ${edge}`,
+    scrollToIndex: (index) => `scroll to index #${index}`,
+    setColumnToValue: (column, value) => `set column ${column} to value ${value}`,
+    setDatePickerDate: (dateString, dateFormat) => `set date picker date to ${dateString} using format ${dateFormat}`,
+    swipe: (direction, speed, normalizedSwipeOffset, normalizedStartingPointX, normalizedStartingPointY) =>
+      `swipe ${direction} ${speed} with offset ${normalizedSwipeOffset}
+      ${!isNaN(normalizedStartingPointX) && !isNaN(normalizedStartingPointY) ? ` from normalized position (${normalizedStartingPointX}, ${normalizedStartingPointY})` : ''}`,
+    takeScreenshot: (screenshotName) => `take screenshot${screenshotName !== undefined ? ` with name "${screenshotName}"` : ''}`,
+    tapAtPoint: (value) => `tap${value !== undefined ? ` at ${JSON.stringify(value)}` : ''}`,
+    tapBackspaceKey: () => 'tap on backspace key',
+    tapReturnKey: () => 'tap on return key',
+    typeText: (value) => `type input text: "${value}"`,
+  },
+  expectDescription: {
+    waitFor: (actionDescription) => `wait for expectation while ${actionDescription}`,
+    waitForWithTimeout: (expectDescription, timeout) => `${expectDescription} with timeout (${timeout} ms)`,
+    withTimeout: (timeout) => `wait until timeout (${timeout} ms)`,
+    toBeFocused: () => 'to be focused',
+    toBeVisible: (percent) => `to be visible${percent !== undefined ? ` ${percent}%` : ''}`,
+    toExist: () => 'to exist',
+    toHaveText: (text) => `to have text: "${text}"`,
+    toHaveLabel: (label) => `to have label: "${label}"`,
+    toHaveId: (id) => `to have id: "${id}"`,
+    toHaveValue: (value) => `to have value: "${value}"`,
+    toHaveSliderPosition: (position, tolerance) => `to have slider position: ${position}${tolerance > 0 ? ` with tolerance ${tolerance}` : ''}`,
+    toHaveToggleValue: (value) => `to have toggle value: ${value}`,
+    full: (expectDescription, notCondition) => `expect element ${notCondition ? `not ${expectDescription}` : expectDescription}`
+  }
+};
diff --git a/detox/src/utils/trace.js b/detox/src/utils/trace.js
index 7dbfdf331..d9a78a40d 100644
--- a/detox/src/utils/trace.js
+++ b/detox/src/utils/trace.js
@@ -1,3 +1,7 @@
+const assert = require('assert');
+
+const TIMELINE_CONTEXT_TYPES = require('../artifacts/timeline/TimelineContextTypes');
+
 class Trace {
   constructor() {
     this.events = [];
@@ -35,20 +39,47 @@ class Trace {
 }
 
 let trace = new Trace();
-async function traceCall(sectionName, func) {
-  trace.startSection(sectionName);
-  try {
-    const result = await func();
-    trace.endSection(sectionName, { success: true });
-    return result;
-  } catch (error) {
-    trace.endSection(sectionName, { success: false, error: error.toString() });
-    throw error;
-  }
+
+function traceCall(sectionName, promise, args = {}) {
+  assert(sectionName,
+    `must provide section name when calling \`traceCall\` with args: \n ${JSON.stringify(args)}`);
+
+  trace.startSection(sectionName, args);
+  return promise
+    .then((result) => {
+      trace.endSection(sectionName, { ...args, success: true });
+      return result;
+    })
+    .catch((error) => {
+      trace.endSection(sectionName, { ...args, success: false, error: error.toString() });
+      throw error;
+    });
+}
+
+function traceInvocationCall(sectionName, invocation, promise) {
+  return traceCall(
+    sectionName, promise, {
+      context: TIMELINE_CONTEXT_TYPES.INVOCATION,
+      stackTrace: _getCallStackTrace(),
+      invocation: JSON.stringify(invocation),
+    });
+}
+
+function _getCallStackTrace() {
+  return new Error().stack
+    .split('\n')
+    .slice(1) // Ignore Error message
+    .map(line => line
+      .replace(/^\s*at\s+/, '')
+      .replace(process.cwd(), '')
+    )
+    .filter(line => !line.includes('/detox/src')) // Ignore detox internal calls
+    .join('\n');
 }
 
 module.exports = {
   Trace,
   trace,
   traceCall,
+  traceInvocationCall
 };


Test patch:
diff --git a/detox/src/android/AndroidExpect.test.js b/detox/src/android/AndroidExpect.test.js
index 54767c6a1..8ed8dfa03 100644
--- a/detox/src/android/AndroidExpect.test.js
+++ b/detox/src/android/AndroidExpect.test.js
@@ -7,6 +7,7 @@ describe('AndroidExpect', () => {
   let device;
 
   beforeEach(() => {
+    jest.mock('../utils/trace');
     jest.mock('tempfile');
     jest.mock('fs-extra');
 
@@ -181,7 +182,9 @@ describe('AndroidExpect', () => {
       });
 
       it('should not tap and long-press given bad args', async () => {
-        await expectToThrow(() => e.element(e.by.id('UniqueId819')).multiTap('NaN'));
+        await [null, undefined, 0, -1, 'NaN'].forEach(item => {
+          expectToThrow(() => e.element(e.by.id('UniqueId819')).multiTap(item));
+        });
       });
 
       it('should press special keys', async () => {
diff --git a/detox/src/artifacts/timeline/TimelineArtifactPlugin.test.js b/detox/src/artifacts/timeline/TimelineArtifactPlugin.test.js
index 812ddda0f..a65a1f168 100644
--- a/detox/src/artifacts/timeline/TimelineArtifactPlugin.test.js
+++ b/detox/src/artifacts/timeline/TimelineArtifactPlugin.test.js
@@ -1,4 +1,6 @@
 const _ = require('lodash');
+
+const TIMELINE_CONTEXT_TYPES = require('./TimelineContextTypes');
 const latestInstanceOf = (clazz) => _.last(clazz.mock.instances);
 
 describe('TimelineArtifactPlugin', () => {
@@ -63,7 +65,7 @@ describe('TimelineArtifactPlugin', () => {
 
     it('should trace a section-start with the suite name on describe-start', async () => {
       await uutEnabled().onRunDescribeStart(suite);
-      expect(trace.startSection).toHaveBeenCalledWith(suite.name, { context: 'describe' });
+      expect(trace.startSection).toHaveBeenCalledWith(suite.name, { context: TIMELINE_CONTEXT_TYPES.DESCRIBE });
     });
 
     it('should save the suite in the plugin-context on describe-start', async () => {
@@ -76,7 +78,7 @@ describe('TimelineArtifactPlugin', () => {
       const uut = uutEnabled();
       await uut.onRunDescribeStart(suite);
       await uut.onRunDescribeFinish(suite);
-      expect(trace.endSection).toHaveBeenCalledWith(suite.name, { context: 'describe' });
+      expect(trace.endSection).toHaveBeenCalledWith(suite.name, { context: TIMELINE_CONTEXT_TYPES.DESCRIBE });
     });
 
     it('should not trace anything if disabled', async () => {
@@ -104,7 +106,7 @@ describe('TimelineArtifactPlugin', () => {
         const uut = uutEnabled();
         await uut.onBootDevice({ deviceId });
         await uut.onRunDescribeStart(rootSuite);
-        expect(trace.startSection).toHaveBeenCalledWith(deviceId, { context: 'describe' });
+        expect(trace.startSection).toHaveBeenCalledWith(deviceId, { context: TIMELINE_CONTEXT_TYPES.DESCRIBE });
         expect(trace.startSection).toHaveBeenCalledTimes(1);
       });
 
@@ -113,7 +115,7 @@ describe('TimelineArtifactPlugin', () => {
         await uut.onBootDevice({ deviceId });
         await uut.onRunDescribeStart(rootSuite);
         await uut.onRunDescribeFinish(rootSuite);
-        expect(trace.endSection).toHaveBeenCalledWith(deviceId, { context: 'describe' });
+        expect(trace.endSection).toHaveBeenCalledWith(deviceId, { context: TIMELINE_CONTEXT_TYPES.DESCRIBE });
         expect(trace.endSection).toHaveBeenCalledTimes(1);
       });
     });
@@ -127,7 +129,7 @@ describe('TimelineArtifactPlugin', () => {
     it('should trace a section-start with the test name on test-start', async () => {
       const uut = uutEnabled();
       await uut.onTestStart(testSummary);
-      expect(trace.startSection).toHaveBeenCalledWith(testSummary.title, { context: 'test' });
+      expect(trace.startSection).toHaveBeenCalledWith(testSummary.title, { context: TIMELINE_CONTEXT_TYPES.TEST });
     });
 
     it('should save the test summary in the plugin\'s context on test-start', async () => {
@@ -139,7 +141,7 @@ describe('TimelineArtifactPlugin', () => {
     it('should trace a section-end with the test name on test-end', async () => {
       const _testSummary = { ...testSummary, status: 'alright!' };
       await uutEnabled().onTestDone(_testSummary);
-      expect(trace.endSection).toHaveBeenCalledWith(testSummary.title, { status: 'alright!', context: 'test' });
+      expect(trace.endSection).toHaveBeenCalledWith(testSummary.title, { status: 'alright!', context: TIMELINE_CONTEXT_TYPES.TEST });
     });
 
     it('should not trace anything if disabled', async () => {
diff --git a/detox/src/devices/allocation/DeviceAllocator.test.js b/detox/src/devices/allocation/DeviceAllocator.test.js
index 490a8e6bb..86a47f4a8 100644
--- a/detox/src/devices/allocation/DeviceAllocator.test.js
+++ b/detox/src/devices/allocation/DeviceAllocator.test.js
@@ -6,8 +6,6 @@ describe('Device allocator', () => {
   let deviceAllocator;
   beforeEach(() => {
     jest.mock('../../utils/trace');
-    const { traceCall } = require('../../utils/trace');
-    traceCall.mockImplementation((__, func) => func());
 
     allocDriver = {
       allocate: jest.fn(),
diff --git a/detox/src/devices/allocation/drivers/android/emulator/EmulatorAllocDriver.test.js b/detox/src/devices/allocation/drivers/android/emulator/EmulatorAllocDriver.test.js
index fe159d92e..dd927f674 100644
--- a/detox/src/devices/allocation/drivers/android/emulator/EmulatorAllocDriver.test.js
+++ b/detox/src/devices/allocation/drivers/android/emulator/EmulatorAllocDriver.test.js
@@ -34,9 +34,7 @@ describe('Allocation driver for Google emulators', () => {
   let emulatorLauncher;
   let allocationHelper;
   beforeEach(() => {
-    jest.mock('../../../../../utils/trace', () => ({
-      traceCall: (name, fn) => fn(),
-    }));
+    jest.mock('../../../../../utils/trace');
 
     jest.mock('../../../../common/drivers/android/exec/ADB');
     const ADB = require('../../../../common/drivers/android/exec/ADB');
diff --git a/detox/src/devices/allocation/drivers/android/emulator/EmulatorLauncher.test.js b/detox/src/devices/allocation/drivers/android/emulator/EmulatorLauncher.test.js
index ab45ed1fe..460800b7d 100644
--- a/detox/src/devices/allocation/drivers/android/emulator/EmulatorLauncher.test.js
+++ b/detox/src/devices/allocation/drivers/android/emulator/EmulatorLauncher.test.js
@@ -16,9 +16,7 @@ describe('Emulator launcher', () => {
     retry = require('../../../../../utils/retry');
     retry.mockImplementation((options, func) => func());
 
-    jest.mock('../../../../../utils/trace', () => ({
-      traceCall: jest.fn().mockImplementation((__, func) => func()),
-    }));
+    jest.mock('../../../../../utils/trace');
 
     const ADB = jest.genMockFromModule('../../../../common/drivers/android/exec/ADB');
     adb = new ADB();
diff --git a/detox/src/ios/expectTwo.test.js b/detox/src/ios/expectTwo.test.js
index 2305e9098..87d99621a 100644
--- a/detox/src/ios/expectTwo.test.js
+++ b/detox/src/ios/expectTwo.test.js
@@ -8,6 +8,7 @@ describe('expectTwo', () => {
   let fs;
 
   beforeEach(() => {
+    jest.mock('../utils/trace');
     jest.mock('fs-extra');
     jest.mock('tempfile');
 
@@ -23,8 +24,8 @@ describe('expectTwo', () => {
     });
   });
 
-  it(`should produce correct JSON for tap action`, () => {
-    const testCall = e.element(e.by.text('tapMe')).tap();
+  it(`should produce correct JSON for tap action`, async () => {
+    const testCall = await e.element(e.by.text('tapMe')).tap();
     const jsonOutput = {
       invocation: {
         type: 'action',
@@ -39,8 +40,8 @@ describe('expectTwo', () => {
     expect(testCall).toDeepEqual(jsonOutput);
   });
 
-  it(`should produce correct JSON for tap action with parameters`, () => {
-    const testCall = e.element(e.by.text('tapMe')).tap({ x: 1, y: 2 });
+  it(`should produce correct JSON for tap action with parameters`, async () => {
+    const testCall = await e.element(e.by.text('tapMe')).tap({ x: 1, y: 2 });
     const jsonOutput = {
       invocation: {
         type: 'action',
@@ -61,8 +62,8 @@ describe('expectTwo', () => {
     expect(testCall).toDeepEqual(jsonOutput);
   });
 
-  it(`should produce correct JSON for element with id and text matchers`, () => {
-    const testCall = e.element(e.by.id('uniqueId').and(e.by.text('some text'))).tap();
+  it(`should produce correct JSON for element with id and text matchers`, async () => {
+    const testCall = await e.element(e.by.id('uniqueId').and(e.by.text('some text'))).tap();
     const jsonOutput = {
       invocation: {
         type: 'action',
@@ -86,8 +87,8 @@ describe('expectTwo', () => {
     expect(testCall).toDeepEqual(jsonOutput);
   });
 
-  it(`should produce correct JSON for element with ancestor matcher`, () => {
-    const testCall = e.element(e.by.id('child').withAncestor(e.by.id('parent'))).tap();
+  it(`should produce correct JSON for element with ancestor matcher`, async () => {
+    const testCall = await e.element(e.by.id('child').withAncestor(e.by.id('parent'))).tap();
     const jsonOutput = {
       invocation: {
         type: 'action',
@@ -114,8 +115,8 @@ describe('expectTwo', () => {
     expect(testCall).toDeepEqual(jsonOutput);
   });
 
-  it(`should produce correct JSON for element with ancestor and index matchers`, () => {
-    const testCall = e.element(e.by.id('child').withAncestor(e.by.id('parent'))).atIndex(0).tap();
+  it(`should produce correct JSON for element with ancestor and index matchers`, async () => {
+    const testCall = await e.element(e.by.id('child').withAncestor(e.by.id('parent'))).atIndex(0).tap();
     const jsonOutput = {
       invocation: {
         type: 'action',
@@ -142,8 +143,8 @@ describe('expectTwo', () => {
     expect(testCall).toDeepEqual(jsonOutput);
   });
 
-  it(`should produce correct JSON for element with ancestor and test matchers`, () => {
-    const testCall = e.element(e.by.id('child').withAncestor(e.by.id('parent').and(e.by.text('text')))).tap();
+  it(`should produce correct JSON for element with ancestor and test matchers`, async () => {
+    const testCall = await e.element(e.by.id('child').withAncestor(e.by.id('parent').and(e.by.text('text')))).tap();
     const jsonOutput = {
       invocation: {
         type: 'action',
@@ -179,8 +180,8 @@ describe('expectTwo', () => {
     expect(testCall).toDeepEqual(jsonOutput);
   });
 
-  it(`should produce correct JSON for element with id, text and value matchers`, () => {
-    const testCall = e.element(e.by.id('child').and(e.by.text('text').and(e.by.value('value')))).tap();
+  it(`should produce correct JSON for element with id, text and value matchers`, async () => {
+    const testCall = await e.element(e.by.id('child').and(e.by.text('text').and(e.by.value('value')))).tap();
     const jsonOutput = {
       invocation: {
         type: 'action',
@@ -208,8 +209,8 @@ describe('expectTwo', () => {
     expect(testCall).toDeepEqual(jsonOutput);
   });
 
-  it(`should produce correct JSON for tap at point action`, () => {
-    const testCall = e.element(e.by.id('tappable')).tapAtPoint({ x: 5, y: 10 });
+  it(`should produce correct JSON for tap at point action`, async () => {
+    const testCall = await e.element(e.by.id('tappable')).tapAtPoint({ x: 5, y: 10 });
     const jsonOutput = {
       invocation: {
         type: 'action',
@@ -230,8 +231,8 @@ describe('expectTwo', () => {
     expect(testCall).toDeepEqual(jsonOutput);
   });
 
-  it(`should produce correct JSON for long-press and drag action`, () => {
-    const testCall = e.element(e.by.id('elementToDrag')).longPressAndDrag(1000, 0.5, 0.5, e.element(e.by.id('targetElement')));
+  it(`should produce correct JSON for long-press and drag action`, async () => {
+    const testCall = await e.element(e.by.id('elementToDrag')).longPressAndDrag(1000, 0.5, 0.5, e.element(e.by.id('targetElement')));
     const jsonOutput = {
       invocation: {
         type: 'action',
@@ -252,8 +253,8 @@ describe('expectTwo', () => {
     expect(testCall).toDeepEqual(jsonOutput);
   });
 
-  it(`should produce correct JSON for visibility expectation`, () => {
-    const testCall = e.expect(e.element(e.by.text('Tap Working!!!'))).toBeVisible();
+  it(`should produce correct JSON for visibility expectation`, async () => {
+    const testCall = await e.expect(e.element(e.by.text('Tap Working!!!'))).toBeVisible();
     const jsonOutput = {
       invocation: {
         type: 'expectation',
@@ -268,8 +269,8 @@ describe('expectTwo', () => {
     expect(testCall).toDeepEqual(jsonOutput);
   });
 
-  it(`should produce correct JSON for toBeVisible expectation with parameter`, () => {
-    const testCall = e.expect(e.element(e.by.id('foo'))).toBeVisible(25);
+  it(`should produce correct JSON for toBeVisible expectation with parameter`, async () => {
+    const testCall = await e.expect(e.element(e.by.id('foo'))).toBeVisible(25);
     const jsonOutput = {
       invocation: {
         type: 'expectation',
@@ -285,8 +286,8 @@ describe('expectTwo', () => {
     expect(testCall).toDeepEqual(jsonOutput);
   });
 
-  it(`should produce correct JSON for toBeNotVisible expectation`, () => {
-    const testCall = e.expect(e.element(e.by.text('Tap Working!!!'))).toBeNotVisible();
+  it(`should produce correct JSON for toBeNotVisible expectation`, async () => {
+    const testCall = await e.expect(e.element(e.by.text('Tap Working!!!'))).toBeNotVisible();
     const jsonOutput = {
       invocation: {
         type: 'expectation',
@@ -302,8 +303,8 @@ describe('expectTwo', () => {
     expect(testCall).toDeepEqual(jsonOutput);
   });
 
-  it(`should produce correct JSON for toBeFocused expectation`, () => {
-    const testCall = e.expect(e.element(e.by.text('Tap Working!!!'))).toBeFocused();
+  it(`should produce correct JSON for toBeFocused expectation`, async () => {
+    const testCall = await e.expect(e.element(e.by.text('Tap Working!!!'))).toBeFocused();
     const jsonOutput = {
       invocation: {
         type: 'expectation',
@@ -318,8 +319,8 @@ describe('expectTwo', () => {
     expect(testCall).toDeepEqual(jsonOutput);
   });
 
-  it(`should produce correct JSON for notToBeFocused expectation`, () => {
-    const testCall = e.expect(e.element(e.by.text('Tap Working!!!'))).toBeNotFocused();
+  it(`should produce correct JSON for notToBeFocused expectation`, async () => {
+    const testCall = await e.expect(e.element(e.by.text('Tap Working!!!'))).toBeNotFocused();
     const jsonOutput = {
       invocation: {
         type: 'expectation',
@@ -335,8 +336,8 @@ describe('expectTwo', () => {
     expect(testCall).toDeepEqual(jsonOutput);
   });
 
-  it(`should produce correct JSON for toHaveText expectation`, () => {
-    const testCall = e.expect(e.element(e.by.id('UniqueId204'))).toHaveText('I contain some text');
+  it(`should produce correct JSON for toHaveText expectation`, async () => {
+    const testCall = await e.expect(e.element(e.by.id('UniqueId204'))).toHaveText('I contain some text');
     const jsonOutput = {
       invocation: {
         type: 'expectation',
@@ -352,8 +353,8 @@ describe('expectTwo', () => {
     expect(testCall).toDeepEqual(jsonOutput);
   });
 
-  it(`should produce correct JSON for toHaveId expectation`, () => {
-    const testCall = e.expect(e.element(e.by.text('Product')).atIndex(2)).toHaveId('ProductId002');
+  it(`should produce correct JSON for toHaveId expectation`, async () => {
+    const testCall = await e.expect(e.element(e.by.text('Product')).atIndex(2)).toHaveId('ProductId002');
     const jsonOutput = {
       'invocation': {
         'type': 'expectation',
@@ -370,8 +371,8 @@ describe('expectTwo', () => {
     expect(testCall).toDeepEqual(jsonOutput);
   });
 
-  it(`should produce correct JSON for toHaveSliderPosition expectation`, () => {
-    const testCall = e.expect(e.element(e.by.id('slider'))).toHaveSliderPosition(0.5, 1);
+  it(`should produce correct JSON for toHaveSliderPosition expectation`, async () => {
+    const testCall = await e.expect(e.element(e.by.id('slider'))).toHaveSliderPosition(0.5, 1);
     const jsonOutput = {
       'invocation': {
         'type': 'expectation',
@@ -387,8 +388,8 @@ describe('expectTwo', () => {
     expect(testCall).toDeepEqual(jsonOutput);
   });
 
-  it(`should produce correct JSON for toHaveToggleValue expectation`, () => {
-    const testCall = e.expect(e.element(e.by.id('switch'))).toHaveToggleValue(true);
+  it(`should produce correct JSON for toHaveToggleValue expectation`, async () => {
+    const testCall = await e.expect(e.element(e.by.id('switch'))).toHaveToggleValue(true);
     const jsonOutput = {
       'invocation': {
         'type': 'expectation',
@@ -404,8 +405,8 @@ describe('expectTwo', () => {
     expect(testCall).toDeepEqual(jsonOutput);
   });
 
-  it(`should produce correct JSON for swipe action`, () => {
-    const testCall = e.element(e.by.id('ScrollView100')).swipe('up', 'fast', undefined, undefined, 0.5);
+  it(`should produce correct JSON for swipe action`, async () => {
+    const testCall = await e.element(e.by.id('ScrollView100')).swipe('up', 'fast', undefined, undefined, 0.5);
     const jsonOutput = {
       invocation: {
         type: 'action',
@@ -422,8 +423,8 @@ describe('expectTwo', () => {
   });
 
   describe(`waitFor`, () => {
-    it(`should produce correct JSON for toBeNotVisible expectation`, () => {
-      const testCall = e.waitFor(e.element(e.by.text('Text5'))).toBeNotVisible().whileElement(e.by.id('ScrollView630')).scroll(50, 'down');
+    it(`should produce correct JSON for toBeNotVisible expectation`, async () => {
+      const testCall = await e.waitFor(e.element(e.by.text('Text5'))).toBeNotVisible().whileElement(e.by.id('ScrollView630')).scroll(50, 'down');
       const jsonOutput = {
         invocation: {
           type: 'action',
@@ -485,8 +486,8 @@ describe('expectTwo', () => {
       expect(testCall).toDeepEqual(jsonOutput);
     });
 
-    it(`should produce correct JSON for toBeNotVisible expectation`, () => {
-      const testCall = e.waitFor(e.element(e.by.id('uniqueId'))).not.toBeVisible().withTimeout(2000);
+    it(`should produce correct JSON for toBeNotVisible expectation`, async () => {
+      const testCall = await e.waitFor(e.element(e.by.id('uniqueId'))).not.toBeVisible().withTimeout(2000);
       const jsonOutput = {
         invocation: {
           type: 'expectation',
@@ -555,15 +556,15 @@ describe('expectTwo', () => {
     });
   });
 
-  it('by.web should throw', () => {
+  it('by.web should throw', async () => {
     expect(() => e.by.web).toThrowError(/not support/);
   });
 
-  it('web() should throw', () => {
+  it('web() should throw', async () => {
     expect(() => e.web(e.by.id('someId'))).toThrowError(/not support/);
   });
 
-  it('web.element() should throw', () => {
+  it('web.element() should throw', async () => {
     expect(() => e.web.element(e.by.id('someId'))).toThrowError(/not support/);
   });
 });
@@ -586,7 +587,8 @@ class MockExecutor {
   constructor() {
     jest.spyOn(this, 'execute');
   }
+
   execute(invocation) {
-    return { invocation };
+    return Promise.resolve({ invocation });
   }
 }
diff --git a/detox/src/ios/expectTwoApiCoverage.test.js b/detox/src/ios/expectTwoApiCoverage.test.js
index 7305971fe..2d38d9eba 100644
--- a/detox/src/ios/expectTwoApiCoverage.test.js
+++ b/detox/src/ios/expectTwoApiCoverage.test.js
@@ -3,7 +3,10 @@ describe('expectTwo API Coverage', () => {
   let e;
 
   beforeEach(() => {
+    jest.mock('../utils/trace');
+
     const IosExpect = require('./expectTwo');
+
     e = new IosExpect({
       invocationManager: new MockExecutor(),
     });
@@ -152,7 +155,9 @@ describe('expectTwo API Coverage', () => {
     });
 
     it(`interactions with wrong parameters should throw`, async () => {
-      await expectToThrow(() => e.element(e.by.id('someId')).multiTap('NaN'));
+      await [null, undefined, 0, -1, 'NaN'].forEach(item => {
+        expectToThrow(() => e.element(e.by.id('someId')).multiTap(item));
+      });
       await expectToThrow(() => e.element(e.by.id('someId')).typeText(0));
       await expectToThrow(() => e.element(e.by.id('someId')).replaceText(3));
 
diff --git a/detox/src/utils/childProcess/exec.test.js b/detox/src/utils/childProcess/exec.test.js
index 9d11a2a18..869f174fb 100644
--- a/detox/src/utils/childProcess/exec.test.js
+++ b/detox/src/utils/childProcess/exec.test.js
@@ -11,9 +11,6 @@ describe('Exec utils', () => {
     cpp = require('child-process-promise');
 
     jest.mock('../trace');
-    require('../trace').traceCall.mockImplementation((s, fn) => {
-      return fn();
-    });
 
     exec = require('./exec');
   });
diff --git a/detox/src/utils/trace.test.js b/detox/src/utils/trace.test.js
index 1547e886f..469bff0fa 100644
--- a/detox/src/utils/trace.test.js
+++ b/detox/src/utils/trace.test.js
@@ -99,11 +99,11 @@ describe('Trace util', () => {
       trace.reset();
     });
 
-    it('should trace a successful function', async () => {
-      const functionCall = () => Promise.resolve(42);
+    it('should trace a successful call', async () => {
+      const promise = Promise.resolve(42);
 
       trace.init();
-      const result = await traceCall(sectionName, functionCall);
+      const result = await traceCall(sectionName, promise);
       expect(trace.events).toEqual([
         expect.any(Object),
         expect.objectContaining(startEventTraits),
@@ -112,12 +112,12 @@ describe('Trace util', () => {
       expect(result).toEqual(42);
     });
 
-    it('should trace a failed function call', async () => {
+    it('should trace a failed call', async () => {
       const error = new Error('error mock');
-      const functionCall = () => Promise.reject(error);
+      const promise = Promise.reject(error);
 
       trace.init();
-      await expect(traceCall(sectionName, functionCall)).rejects.toThrowError(error);
+      await expect(traceCall(sectionName, promise)).rejects.toThrowError(error);
       expect(trace.events).toEqual([
         expect.any(Object),
         expect.objectContaining(startEventTraits),
@@ -125,4 +125,70 @@ describe('Trace util', () => {
       ]);
     });
   });
+
+  describe('trace-invocation-call function', () => {
+    const { trace, traceInvocationCall } = require('./trace');
+    const sectionName = 'section-name';
+    const invocation = {
+      foo: 'bar'
+    };
+    const args = {
+      context: 'invocation',
+      invocation: JSON.stringify(invocation),
+      stackTrace: expect.any(String)
+    };
+
+    afterEach(() => {
+      trace.reset();
+    });
+
+    it('should trace a successful invocation call', async () => {
+      const promise = Promise.resolve(42);
+
+      trace.init();
+      const result = await traceInvocationCall(sectionName, invocation, promise);
+
+      const expectedStartEvent = {
+        type: 'start',
+        name: sectionName,
+        args
+      };
+      const expectedEndEvent = {
+        type: 'end',
+        name: sectionName,
+        args: { ...args, success: true },
+      };
+      expect(trace.events).toEqual([
+        expect.any(Object),
+        expect.objectContaining(expectedStartEvent),
+        expect.objectContaining(expectedEndEvent),
+      ]);
+      expect(result).toEqual(42);
+    });
+
+    it('should trace a failed invocation call', async () => {
+      const error = new Error('error mock');
+      const promise = Promise.reject(error);
+
+      trace.init();
+      await expect(traceInvocationCall(sectionName, invocation, promise)).rejects.toThrowError(error);
+
+      const expectedStartEvent = {
+        type: 'start',
+        name: sectionName,
+        args
+      };
+      const expectedEndEvent = {
+        type: 'end',
+        name: sectionName,
+        args: { ...args, error: 'Error: error mock', success: false },
+      };
+      expect(trace.events).toEqual([
+        expect.any(Object),
+        expect.objectContaining(expectedStartEvent),
+        expect.objectContaining(expectedEndEvent),
+      ]);
+    });
+  });
+
 });
diff --git a/detox/test/e2e/01.sanity.test.js b/detox/test/e2e/01.sanity.test.js
index 31c0b2580..d99fb8296 100644
--- a/detox/test/e2e/01.sanity.test.js
+++ b/detox/test/e2e/01.sanity.test.js
@@ -1,7 +1,7 @@
 describe('Sanity', () => {
   beforeEach(async () => {
     await device.reloadReactNative();
-    await detox.traceCall('Navigate to sanity', () => element(by.text('Sanity')).tap());
+    await detox.traceCall('Navigate to sanity', element(by.text('Sanity')).tap());
   });
 
   it('should have welcome screen', async () => {
diff --git a/detox/test/integration/__snapshots__/timeline-artifact.test.js.snap b/detox/test/integration/__snapshots__/timeline-artifact.test.js.snap
index 8c9f71089..7d7c87138 100644
--- a/detox/test/integration/__snapshots__/timeline-artifact.test.js.snap
+++ b/detox/test/integration/__snapshots__/timeline-artifact.test.js.snap
@@ -1,3 +1,3 @@
 // Jest Snapshot v1, https://goo.gl/fbAQLP
 
-exports[`Timeline integration test should deterministically produce a timeline artifact 1`] = `"[{\\"name\\":\\"process_name\\",\\"pid\\":0,\\"tid\\":\\"2\\",\\"ts\\":1000000,\\"ph\\":\\"M\\",\\"args\\":{\\"name\\":\\"detox\\"}},{\\"name\\":\\"thread_name\\",\\"pid\\":0,\\"tid\\":\\"2\\",\\"ts\\":1000000,\\"ph\\":\\"M\\",\\"args\\":{\\"name\\":\\"Worker #2\\"}},{\\"name\\":\\"detoxInit\\",\\"pid\\":0,\\"tid\\":\\"2\\",\\"ts\\":1100000,\\"ph\\":\\"B\\",\\"args\\":{}},{\\"name\\":\\"allocateDevice\\",\\"pid\\":0,\\"tid\\":\\"2\\",\\"ts\\":1200000,\\"ph\\":\\"B\\",\\"args\\":{}},{\\"name\\":\\"allocateDevice\\",\\"pid\\":0,\\"tid\\":\\"2\\",\\"ts\\":1300000,\\"ph\\":\\"E\\",\\"args\\":{\\"success\\":true}},{\\"name\\":\\"appUninstall\\",\\"pid\\":0,\\"tid\\":\\"2\\",\\"ts\\":1400000,\\"ph\\":\\"B\\",\\"args\\":{}},{\\"name\\":\\"appUninstall\\",\\"pid\\":0,\\"tid\\":\\"2\\",\\"ts\\":1500000,\\"ph\\":\\"E\\",\\"args\\":{\\"success\\":true}},{\\"name\\":\\"appInstall\\",\\"pid\\":0,\\"tid\\":\\"2\\",\\"ts\\":1600000,\\"ph\\":\\"B\\",\\"args\\":{}},{\\"name\\":\\"appInstall\\",\\"pid\\":0,\\"tid\\":\\"2\\",\\"ts\\":1700000,\\"ph\\":\\"E\\",\\"args\\":{\\"success\\":true}},{\\"name\\":\\"detoxInit\\",\\"pid\\":0,\\"tid\\":\\"2\\",\\"ts\\":1800000,\\"ph\\":\\"E\\",\\"args\\":{\\"success\\":true}},{\\"name\\":\\"StubDevice#2\\",\\"pid\\":0,\\"tid\\":\\"2\\",\\"ts\\":1900000,\\"ph\\":\\"B\\",\\"args\\":{\\"context\\":\\"describe\\"}},{\\"name\\":\\"Stub1\\",\\"pid\\":0,\\"tid\\":\\"2\\",\\"ts\\":2000000,\\"ph\\":\\"B\\",\\"args\\":{\\"context\\":\\"describe\\"}},{\\"name\\":\\"should have welcome screen\\",\\"pid\\":0,\\"tid\\":\\"2\\",\\"ts\\":2100000,\\"ph\\":\\"B\\",\\"args\\":{\\"context\\":\\"test\\"}},{\\"name\\":\\"reloadRN\\",\\"pid\\":0,\\"tid\\":\\"2\\",\\"ts\\":2200000,\\"ph\\":\\"B\\",\\"args\\":{}},{\\"name\\":\\"reloadRN\\",\\"pid\\":0,\\"tid\\":\\"2\\",\\"ts\\":2300000,\\"ph\\":\\"E\\",\\"args\\":{\\"success\\":true}},{\\"name\\":\\"Navigate to sanity\\",\\"pid\\":0,\\"tid\\":\\"2\\",\\"ts\\":2400000,\\"ph\\":\\"B\\",\\"args\\":{}},{\\"name\\":\\"Navigate to sanity\\",\\"pid\\":0,\\"tid\\":\\"2\\",\\"ts\\":2500000,\\"ph\\":\\"E\\",\\"args\\":{\\"success\\":true}},{\\"name\\":\\"Asserting various texts\\",\\"pid\\":0,\\"tid\\":\\"2\\",\\"ts\\":2600000,\\"ph\\":\\"B\\",\\"args\\":{}},{\\"name\\":\\"Asserting various texts\\",\\"pid\\":0,\\"tid\\":\\"2\\",\\"ts\\":2700000,\\"ph\\":\\"E\\",\\"args\\":{}},{\\"name\\":\\"should have welcome screen\\",\\"pid\\":0,\\"tid\\":\\"2\\",\\"ts\\":2800000,\\"ph\\":\\"E\\",\\"args\\":{\\"status\\":\\"failed\\",\\"context\\":\\"test\\"}},{\\"name\\":\\"Stub1\\",\\"pid\\":0,\\"tid\\":\\"2\\",\\"ts\\":2900000,\\"ph\\":\\"E\\",\\"args\\":{\\"context\\":\\"describe\\"}},{\\"name\\":\\"StubDevice#2\\",\\"pid\\":0,\\"tid\\":\\"2\\",\\"ts\\":3000000,\\"ph\\":\\"E\\",\\"args\\":{\\"context\\":\\"describe\\"}},{\\"name\\":\\"process_name\\",\\"pid\\":0,\\"tid\\":\\"1\\",\\"ts\\":1000000,\\"ph\\":\\"M\\",\\"args\\":{\\"name\\":\\"detox\\"}},{\\"name\\":\\"thread_name\\",\\"pid\\":0,\\"tid\\":\\"1\\",\\"ts\\":1000000,\\"ph\\":\\"M\\",\\"args\\":{\\"name\\":\\"Worker #1\\"}},{\\"name\\":\\"detoxInit\\",\\"pid\\":0,\\"tid\\":\\"1\\",\\"ts\\":1100000,\\"ph\\":\\"B\\",\\"args\\":{}},{\\"name\\":\\"allocateDevice\\",\\"pid\\":0,\\"tid\\":\\"1\\",\\"ts\\":1200000,\\"ph\\":\\"B\\",\\"args\\":{}},{\\"name\\":\\"allocateDevice\\",\\"pid\\":0,\\"tid\\":\\"1\\",\\"ts\\":1300000,\\"ph\\":\\"E\\",\\"args\\":{\\"success\\":true}},{\\"name\\":\\"appUninstall\\",\\"pid\\":0,\\"tid\\":\\"1\\",\\"ts\\":1400000,\\"ph\\":\\"B\\",\\"args\\":{}},{\\"name\\":\\"appUninstall\\",\\"pid\\":0,\\"tid\\":\\"1\\",\\"ts\\":1500000,\\"ph\\":\\"E\\",\\"args\\":{\\"success\\":true}},{\\"name\\":\\"appInstall\\",\\"pid\\":0,\\"tid\\":\\"1\\",\\"ts\\":1600000,\\"ph\\":\\"B\\",\\"args\\":{}},{\\"name\\":\\"appInstall\\",\\"pid\\":0,\\"tid\\":\\"1\\",\\"ts\\":1700000,\\"ph\\":\\"E\\",\\"args\\":{\\"success\\":true}},{\\"name\\":\\"detoxInit\\",\\"pid\\":0,\\"tid\\":\\"1\\",\\"ts\\":1800000,\\"ph\\":\\"E\\",\\"args\\":{\\"success\\":true}},{\\"name\\":\\"StubDevice#1\\",\\"pid\\":0,\\"tid\\":\\"1\\",\\"ts\\":1900000,\\"ph\\":\\"B\\",\\"args\\":{\\"context\\":\\"describe\\"}},{\\"name\\":\\"Stub3\\",\\"pid\\":0,\\"tid\\":\\"1\\",\\"ts\\":2000000,\\"ph\\":\\"B\\",\\"args\\":{\\"context\\":\\"describe\\"}},{\\"name\\":\\"should have welcome screen\\",\\"pid\\":0,\\"tid\\":\\"1\\",\\"ts\\":2100000,\\"ph\\":\\"B\\",\\"args\\":{\\"context\\":\\"test\\"}},{\\"name\\":\\"reloadRN\\",\\"pid\\":0,\\"tid\\":\\"1\\",\\"ts\\":2200000,\\"ph\\":\\"B\\",\\"args\\":{}},{\\"name\\":\\"reloadRN\\",\\"pid\\":0,\\"tid\\":\\"1\\",\\"ts\\":2300000,\\"ph\\":\\"E\\",\\"args\\":{\\"success\\":true}},{\\"name\\":\\"should have welcome screen\\",\\"pid\\":0,\\"tid\\":\\"1\\",\\"ts\\":2400000,\\"ph\\":\\"E\\",\\"args\\":{\\"status\\":\\"passed\\",\\"context\\":\\"test\\"}},{\\"name\\":\\"should show hello screen after tap\\",\\"pid\\":0,\\"tid\\":\\"1\\",\\"ts\\":2500000,\\"ph\\":\\"B\\",\\"args\\":{\\"context\\":\\"test\\"}},{\\"name\\":\\"reloadRN\\",\\"pid\\":0,\\"tid\\":\\"1\\",\\"ts\\":2600000,\\"ph\\":\\"B\\",\\"args\\":{}},{\\"name\\":\\"reloadRN\\",\\"pid\\":0,\\"tid\\":\\"1\\",\\"ts\\":2700000,\\"ph\\":\\"E\\",\\"args\\":{\\"success\\":true}},{\\"name\\":\\"should show hello screen after tap\\",\\"pid\\":0,\\"tid\\":\\"1\\",\\"ts\\":2800000,\\"ph\\":\\"E\\",\\"args\\":{\\"status\\":\\"passed\\",\\"context\\":\\"test\\"}},{\\"name\\":\\"should show world screen after tap\\",\\"pid\\":0,\\"tid\\":\\"1\\",\\"ts\\":2900000,\\"ph\\":\\"B\\",\\"args\\":{\\"context\\":\\"test\\"}},{\\"name\\":\\"reloadRN\\",\\"pid\\":0,\\"tid\\":\\"1\\",\\"ts\\":3000000,\\"ph\\":\\"B\\",\\"args\\":{}},{\\"name\\":\\"reloadRN\\",\\"pid\\":0,\\"tid\\":\\"1\\",\\"ts\\":3100000,\\"ph\\":\\"E\\",\\"args\\":{\\"success\\":true}},{\\"name\\":\\"should show world screen after tap\\",\\"pid\\":0,\\"tid\\":\\"1\\",\\"ts\\":3200000,\\"ph\\":\\"E\\",\\"args\\":{\\"status\\":\\"passed\\",\\"context\\":\\"test\\"}},{\\"name\\":\\"Stub3\\",\\"pid\\":0,\\"tid\\":\\"1\\",\\"ts\\":3300000,\\"ph\\":\\"E\\",\\"args\\":{\\"context\\":\\"describe\\"}},{\\"name\\":\\"StubDevice#1\\",\\"pid\\":0,\\"tid\\":\\"1\\",\\"ts\\":3400000,\\"ph\\":\\"E\\",\\"args\\":{\\"context\\":\\"describe\\"}},{\\"name\\":\\"process_name\\",\\"pid\\":0,\\"tid\\":\\"2\\",\\"ts\\":3100000,\\"ph\\":\\"M\\",\\"args\\":{\\"name\\":\\"detox\\"}},{\\"name\\":\\"thread_name\\",\\"pid\\":0,\\"tid\\":\\"2\\",\\"ts\\":3100000,\\"ph\\":\\"M\\",\\"args\\":{\\"name\\":\\"Worker #2\\"}},{\\"name\\":\\"detoxInit\\",\\"pid\\":0,\\"tid\\":\\"2\\",\\"ts\\":3200000,\\"ph\\":\\"B\\",\\"args\\":{}},{\\"name\\":\\"allocateDevice\\",\\"pid\\":0,\\"tid\\":\\"2\\",\\"ts\\":3300000,\\"ph\\":\\"B\\",\\"args\\":{}},{\\"name\\":\\"allocateDevice\\",\\"pid\\":0,\\"tid\\":\\"2\\",\\"ts\\":3400000,\\"ph\\":\\"E\\",\\"args\\":{\\"success\\":true}},{\\"name\\":\\"appUninstall\\",\\"pid\\":0,\\"tid\\":\\"2\\",\\"ts\\":3500000,\\"ph\\":\\"B\\",\\"args\\":{}},{\\"name\\":\\"appUninstall\\",\\"pid\\":0,\\"tid\\":\\"2\\",\\"ts\\":3600000,\\"ph\\":\\"E\\",\\"args\\":{\\"success\\":true}},{\\"name\\":\\"appInstall\\",\\"pid\\":0,\\"tid\\":\\"2\\",\\"ts\\":3700000,\\"ph\\":\\"B\\",\\"args\\":{}},{\\"name\\":\\"appInstall\\",\\"pid\\":0,\\"tid\\":\\"2\\",\\"ts\\":3800000,\\"ph\\":\\"E\\",\\"args\\":{\\"success\\":true}},{\\"name\\":\\"detoxInit\\",\\"pid\\":0,\\"tid\\":\\"2\\",\\"ts\\":3900000,\\"ph\\":\\"E\\",\\"args\\":{\\"success\\":true}},{\\"name\\":\\"StubDevice#2\\",\\"pid\\":0,\\"tid\\":\\"2\\",\\"ts\\":4000000,\\"ph\\":\\"B\\",\\"args\\":{\\"context\\":\\"describe\\"}},{\\"name\\":\\"Stub2\\",\\"pid\\":0,\\"tid\\":\\"2\\",\\"ts\\":4100000,\\"ph\\":\\"B\\",\\"args\\":{\\"context\\":\\"describe\\"}},{\\"name\\":\\"should have welcome screen\\",\\"pid\\":0,\\"tid\\":\\"2\\",\\"ts\\":4200000,\\"ph\\":\\"B\\",\\"args\\":{\\"context\\":\\"test\\"}},{\\"name\\":\\"reloadRN\\",\\"pid\\":0,\\"tid\\":\\"2\\",\\"ts\\":4300000,\\"ph\\":\\"B\\",\\"args\\":{}},{\\"name\\":\\"reloadRN\\",\\"pid\\":0,\\"tid\\":\\"2\\",\\"ts\\":4400000,\\"ph\\":\\"E\\",\\"args\\":{\\"success\\":true}},{\\"name\\":\\"should have welcome screen\\",\\"pid\\":0,\\"tid\\":\\"2\\",\\"ts\\":4500000,\\"ph\\":\\"E\\",\\"args\\":{\\"status\\":\\"passed\\",\\"context\\":\\"test\\"}},{\\"name\\":\\"should show hello screen after tap\\",\\"pid\\":0,\\"tid\\":\\"2\\",\\"ts\\":4600000,\\"ph\\":\\"B\\",\\"args\\":{\\"context\\":\\"test\\"}},{\\"name\\":\\"reloadRN\\",\\"pid\\":0,\\"tid\\":\\"2\\",\\"ts\\":4700000,\\"ph\\":\\"B\\",\\"args\\":{}},{\\"name\\":\\"reloadRN\\",\\"pid\\":0,\\"tid\\":\\"2\\",\\"ts\\":4800000,\\"ph\\":\\"E\\",\\"args\\":{\\"success\\":true}},{\\"name\\":\\"should show hello screen after tap\\",\\"pid\\":0,\\"tid\\":\\"2\\",\\"ts\\":4900000,\\"ph\\":\\"E\\",\\"args\\":{\\"status\\":\\"passed\\",\\"context\\":\\"test\\"}},{\\"name\\":\\"Stub2\\",\\"pid\\":0,\\"tid\\":\\"2\\",\\"ts\\":5000000,\\"ph\\":\\"E\\",\\"args\\":{\\"context\\":\\"describe\\"}},{\\"name\\":\\"StubDevice#2\\",\\"pid\\":0,\\"tid\\":\\"2\\",\\"ts\\":5100000,\\"ph\\":\\"E\\",\\"args\\":{\\"context\\":\\"describe\\"}}"`;
+exports[`Timeline integration test should deterministically produce a timeline artifact 1`] = `"[{\\"name\\":\\"process_name\\",\\"pid\\":0,\\"tid\\":\\"2\\",\\"ts\\":1000000,\\"ph\\":\\"M\\",\\"args\\":{\\"name\\":\\"detox\\"}},{\\"name\\":\\"thread_name\\",\\"pid\\":0,\\"tid\\":\\"2\\",\\"ts\\":1000000,\\"ph\\":\\"M\\",\\"args\\":{\\"name\\":\\"Worker #2\\"}},{\\"name\\":\\"detoxInit\\",\\"pid\\":0,\\"tid\\":\\"2\\",\\"ts\\":1100000,\\"ph\\":\\"B\\",\\"args\\":{}},{\\"name\\":\\"allocateDevice\\",\\"pid\\":0,\\"tid\\":\\"2\\",\\"ts\\":1200000,\\"ph\\":\\"B\\",\\"args\\":{}},{\\"name\\":\\"allocateDevice\\",\\"pid\\":0,\\"tid\\":\\"2\\",\\"ts\\":1300000,\\"ph\\":\\"E\\",\\"args\\":{\\"success\\":true}},{\\"name\\":\\"appUninstall\\",\\"pid\\":0,\\"tid\\":\\"2\\",\\"ts\\":1400000,\\"ph\\":\\"B\\",\\"args\\":{}},{\\"name\\":\\"appUninstall\\",\\"pid\\":0,\\"tid\\":\\"2\\",\\"ts\\":1500000,\\"ph\\":\\"E\\",\\"args\\":{\\"success\\":true}},{\\"name\\":\\"appInstall\\",\\"pid\\":0,\\"tid\\":\\"2\\",\\"ts\\":1600000,\\"ph\\":\\"B\\",\\"args\\":{}},{\\"name\\":\\"appInstall\\",\\"pid\\":0,\\"tid\\":\\"2\\",\\"ts\\":1700000,\\"ph\\":\\"E\\",\\"args\\":{\\"success\\":true}},{\\"name\\":\\"detoxInit\\",\\"pid\\":0,\\"tid\\":\\"2\\",\\"ts\\":1800000,\\"ph\\":\\"E\\",\\"args\\":{\\"success\\":true}},{\\"name\\":\\"StubDevice#2\\",\\"pid\\":0,\\"tid\\":\\"2\\",\\"ts\\":1900000,\\"ph\\":\\"B\\",\\"args\\":{\\"context\\":\\"describe\\"}},{\\"name\\":\\"Stub1\\",\\"pid\\":0,\\"tid\\":\\"2\\",\\"ts\\":2000000,\\"ph\\":\\"B\\",\\"args\\":{\\"context\\":\\"describe\\"}},{\\"name\\":\\"should have welcome screen\\",\\"pid\\":0,\\"tid\\":\\"2\\",\\"ts\\":2100000,\\"ph\\":\\"B\\",\\"args\\":{\\"context\\":\\"test\\"}},{\\"name\\":\\"reload React Native\\",\\"pid\\":0,\\"tid\\":\\"2\\",\\"ts\\":2200000,\\"ph\\":\\"B\\",\\"args\\":{}},{\\"name\\":\\"reload React Native\\",\\"pid\\":0,\\"tid\\":\\"2\\",\\"ts\\":2300000,\\"ph\\":\\"E\\",\\"args\\":{\\"success\\":true}},{\\"name\\":\\"Navigate to sanity\\",\\"pid\\":0,\\"tid\\":\\"2\\",\\"ts\\":2400000,\\"ph\\":\\"B\\",\\"args\\":{}},{\\"name\\":\\"Navigate to sanity\\",\\"pid\\":0,\\"tid\\":\\"2\\",\\"ts\\":2500000,\\"ph\\":\\"E\\",\\"args\\":{\\"success\\":true}},{\\"name\\":\\"Asserting various texts\\",\\"pid\\":0,\\"tid\\":\\"2\\",\\"ts\\":2600000,\\"ph\\":\\"B\\",\\"args\\":{}},{\\"name\\":\\"Asserting various texts\\",\\"pid\\":0,\\"tid\\":\\"2\\",\\"ts\\":2700000,\\"ph\\":\\"E\\",\\"args\\":{}},{\\"name\\":\\"should have welcome screen\\",\\"pid\\":0,\\"tid\\":\\"2\\",\\"ts\\":2800000,\\"ph\\":\\"E\\",\\"args\\":{\\"status\\":\\"failed\\",\\"context\\":\\"test\\"}},{\\"name\\":\\"Stub1\\",\\"pid\\":0,\\"tid\\":\\"2\\",\\"ts\\":2900000,\\"ph\\":\\"E\\",\\"args\\":{\\"context\\":\\"describe\\"}},{\\"name\\":\\"StubDevice#2\\",\\"pid\\":0,\\"tid\\":\\"2\\",\\"ts\\":3000000,\\"ph\\":\\"E\\",\\"args\\":{\\"context\\":\\"describe\\"}},{\\"name\\":\\"process_name\\",\\"pid\\":0,\\"tid\\":\\"1\\",\\"ts\\":1000000,\\"ph\\":\\"M\\",\\"args\\":{\\"name\\":\\"detox\\"}},{\\"name\\":\\"thread_name\\",\\"pid\\":0,\\"tid\\":\\"1\\",\\"ts\\":1000000,\\"ph\\":\\"M\\",\\"args\\":{\\"name\\":\\"Worker #1\\"}},{\\"name\\":\\"detoxInit\\",\\"pid\\":0,\\"tid\\":\\"1\\",\\"ts\\":1100000,\\"ph\\":\\"B\\",\\"args\\":{}},{\\"name\\":\\"allocateDevice\\",\\"pid\\":0,\\"tid\\":\\"1\\",\\"ts\\":1200000,\\"ph\\":\\"B\\",\\"args\\":{}},{\\"name\\":\\"allocateDevice\\",\\"pid\\":0,\\"tid\\":\\"1\\",\\"ts\\":1300000,\\"ph\\":\\"E\\",\\"args\\":{\\"success\\":true}},{\\"name\\":\\"appUninstall\\",\\"pid\\":0,\\"tid\\":\\"1\\",\\"ts\\":1400000,\\"ph\\":\\"B\\",\\"args\\":{}},{\\"name\\":\\"appUninstall\\",\\"pid\\":0,\\"tid\\":\\"1\\",\\"ts\\":1500000,\\"ph\\":\\"E\\",\\"args\\":{\\"success\\":true}},{\\"name\\":\\"appInstall\\",\\"pid\\":0,\\"tid\\":\\"1\\",\\"ts\\":1600000,\\"ph\\":\\"B\\",\\"args\\":{}},{\\"name\\":\\"appInstall\\",\\"pid\\":0,\\"tid\\":\\"1\\",\\"ts\\":1700000,\\"ph\\":\\"E\\",\\"args\\":{\\"success\\":true}},{\\"name\\":\\"detoxInit\\",\\"pid\\":0,\\"tid\\":\\"1\\",\\"ts\\":1800000,\\"ph\\":\\"E\\",\\"args\\":{\\"success\\":true}},{\\"name\\":\\"StubDevice#1\\",\\"pid\\":0,\\"tid\\":\\"1\\",\\"ts\\":1900000,\\"ph\\":\\"B\\",\\"args\\":{\\"context\\":\\"describe\\"}},{\\"name\\":\\"Stub3\\",\\"pid\\":0,\\"tid\\":\\"1\\",\\"ts\\":2000000,\\"ph\\":\\"B\\",\\"args\\":{\\"context\\":\\"describe\\"}},{\\"name\\":\\"should have welcome screen\\",\\"pid\\":0,\\"tid\\":\\"1\\",\\"ts\\":2100000,\\"ph\\":\\"B\\",\\"args\\":{\\"context\\":\\"test\\"}},{\\"name\\":\\"reload React Native\\",\\"pid\\":0,\\"tid\\":\\"1\\",\\"ts\\":2200000,\\"ph\\":\\"B\\",\\"args\\":{}},{\\"name\\":\\"reload React Native\\",\\"pid\\":0,\\"tid\\":\\"1\\",\\"ts\\":2300000,\\"ph\\":\\"E\\",\\"args\\":{\\"success\\":true}},{\\"name\\":\\"should have welcome screen\\",\\"pid\\":0,\\"tid\\":\\"1\\",\\"ts\\":2400000,\\"ph\\":\\"E\\",\\"args\\":{\\"status\\":\\"passed\\",\\"context\\":\\"test\\"}},{\\"name\\":\\"should show hello screen after tap\\",\\"pid\\":0,\\"tid\\":\\"1\\",\\"ts\\":2500000,\\"ph\\":\\"B\\",\\"args\\":{\\"context\\":\\"test\\"}},{\\"name\\":\\"reload React Native\\",\\"pid\\":0,\\"tid\\":\\"1\\",\\"ts\\":2600000,\\"ph\\":\\"B\\",\\"args\\":{}},{\\"name\\":\\"reload React Native\\",\\"pid\\":0,\\"tid\\":\\"1\\",\\"ts\\":2700000,\\"ph\\":\\"E\\",\\"args\\":{\\"success\\":true}},{\\"name\\":\\"should show hello screen after tap\\",\\"pid\\":0,\\"tid\\":\\"1\\",\\"ts\\":2800000,\\"ph\\":\\"E\\",\\"args\\":{\\"status\\":\\"passed\\",\\"context\\":\\"test\\"}},{\\"name\\":\\"should show world screen after tap\\",\\"pid\\":0,\\"tid\\":\\"1\\",\\"ts\\":2900000,\\"ph\\":\\"B\\",\\"args\\":{\\"context\\":\\"test\\"}},{\\"name\\":\\"reload React Native\\",\\"pid\\":0,\\"tid\\":\\"1\\",\\"ts\\":3000000,\\"ph\\":\\"B\\",\\"args\\":{}},{\\"name\\":\\"reload React Native\\",\\"pid\\":0,\\"tid\\":\\"1\\",\\"ts\\":3100000,\\"ph\\":\\"E\\",\\"args\\":{\\"success\\":true}},{\\"name\\":\\"should show world screen after tap\\",\\"pid\\":0,\\"tid\\":\\"1\\",\\"ts\\":3200000,\\"ph\\":\\"E\\",\\"args\\":{\\"status\\":\\"passed\\",\\"context\\":\\"test\\"}},{\\"name\\":\\"Stub3\\",\\"pid\\":0,\\"tid\\":\\"1\\",\\"ts\\":3300000,\\"ph\\":\\"E\\",\\"args\\":{\\"context\\":\\"describe\\"}},{\\"name\\":\\"StubDevice#1\\",\\"pid\\":0,\\"tid\\":\\"1\\",\\"ts\\":3400000,\\"ph\\":\\"E\\",\\"args\\":{\\"context\\":\\"describe\\"}},{\\"name\\":\\"process_name\\",\\"pid\\":0,\\"tid\\":\\"2\\",\\"ts\\":3100000,\\"ph\\":\\"M\\",\\"args\\":{\\"name\\":\\"detox\\"}},{\\"name\\":\\"thread_name\\",\\"pid\\":0,\\"tid\\":\\"2\\",\\"ts\\":3100000,\\"ph\\":\\"M\\",\\"args\\":{\\"name\\":\\"Worker #2\\"}},{\\"name\\":\\"detoxInit\\",\\"pid\\":0,\\"tid\\":\\"2\\",\\"ts\\":3200000,\\"ph\\":\\"B\\",\\"args\\":{}},{\\"name\\":\\"allocateDevice\\",\\"pid\\":0,\\"tid\\":\\"2\\",\\"ts\\":3300000,\\"ph\\":\\"B\\",\\"args\\":{}},{\\"name\\":\\"allocateDevice\\",\\"pid\\":0,\\"tid\\":\\"2\\",\\"ts\\":3400000,\\"ph\\":\\"E\\",\\"args\\":{\\"success\\":true}},{\\"name\\":\\"appUninstall\\",\\"pid\\":0,\\"tid\\":\\"2\\",\\"ts\\":3500000,\\"ph\\":\\"B\\",\\"args\\":{}},{\\"name\\":\\"appUninstall\\",\\"pid\\":0,\\"tid\\":\\"2\\",\\"ts\\":3600000,\\"ph\\":\\"E\\",\\"args\\":{\\"success\\":true}},{\\"name\\":\\"appInstall\\",\\"pid\\":0,\\"tid\\":\\"2\\",\\"ts\\":3700000,\\"ph\\":\\"B\\",\\"args\\":{}},{\\"name\\":\\"appInstall\\",\\"pid\\":0,\\"tid\\":\\"2\\",\\"ts\\":3800000,\\"ph\\":\\"E\\",\\"args\\":{\\"success\\":true}},{\\"name\\":\\"detoxInit\\",\\"pid\\":0,\\"tid\\":\\"2\\",\\"ts\\":3900000,\\"ph\\":\\"E\\",\\"args\\":{\\"success\\":true}},{\\"name\\":\\"StubDevice#2\\",\\"pid\\":0,\\"tid\\":\\"2\\",\\"ts\\":4000000,\\"ph\\":\\"B\\",\\"args\\":{\\"context\\":\\"describe\\"}},{\\"name\\":\\"Stub2\\",\\"pid\\":0,\\"tid\\":\\"2\\",\\"ts\\":4100000,\\"ph\\":\\"B\\",\\"args\\":{\\"context\\":\\"describe\\"}},{\\"name\\":\\"should have welcome screen\\",\\"pid\\":0,\\"tid\\":\\"2\\",\\"ts\\":4200000,\\"ph\\":\\"B\\",\\"args\\":{\\"context\\":\\"test\\"}},{\\"name\\":\\"reload React Native\\",\\"pid\\":0,\\"tid\\":\\"2\\",\\"ts\\":4300000,\\"ph\\":\\"B\\",\\"args\\":{}},{\\"name\\":\\"reload React Native\\",\\"pid\\":0,\\"tid\\":\\"2\\",\\"ts\\":4400000,\\"ph\\":\\"E\\",\\"args\\":{\\"success\\":true}},{\\"name\\":\\"should have welcome screen\\",\\"pid\\":0,\\"tid\\":\\"2\\",\\"ts\\":4500000,\\"ph\\":\\"E\\",\\"args\\":{\\"status\\":\\"passed\\",\\"context\\":\\"test\\"}},{\\"name\\":\\"should show hello screen after tap\\",\\"pid\\":0,\\"tid\\":\\"2\\",\\"ts\\":4600000,\\"ph\\":\\"B\\",\\"args\\":{\\"context\\":\\"test\\"}},{\\"name\\":\\"reload React Native\\",\\"pid\\":0,\\"tid\\":\\"2\\",\\"ts\\":4700000,\\"ph\\":\\"B\\",\\"args\\":{}},{\\"name\\":\\"reload React Native\\",\\"pid\\":0,\\"tid\\":\\"2\\",\\"ts\\":4800000,\\"ph\\":\\"E\\",\\"args\\":{\\"success\\":true}},{\\"name\\":\\"should show hello screen after tap\\",\\"pid\\":0,\\"tid\\":\\"2\\",\\"ts\\":4900000,\\"ph\\":\\"E\\",\\"args\\":{\\"status\\":\\"passed\\",\\"context\\":\\"test\\"}},{\\"name\\":\\"Stub2\\",\\"pid\\":0,\\"tid\\":\\"2\\",\\"ts\\":5000000,\\"ph\\":\\"E\\",\\"args\\":{\\"context\\":\\"describe\\"}},{\\"name\\":\\"StubDevice#2\\",\\"pid\\":0,\\"tid\\":\\"2\\",\\"ts\\":5100000,\\"ph\\":\\"E\\",\\"args\\":{\\"context\\":\\"describe\\"}}"`;
diff --git a/detox/test/integration/e2e/stub1.test.js b/detox/test/integration/e2e/stub1.test.js
index 7e593c4b4..bc9755aca 100644
--- a/detox/test/integration/e2e/stub1.test.js
+++ b/detox/test/integration/e2e/stub1.test.js
@@ -1,7 +1,7 @@
 describe('Stub1', () => {
   beforeEach(async () => {
     await device.reloadReactNative();
-    await detox.traceCall('Navigate to sanity', () => element(by.text('Sanity')).tap());
+    await detox.traceCall('Navigate to sanity', element(by.text('Sanity')).tap());
   });
 
   it('should have welcome screen', async () => {