Tracking System function (Get) calls, and Stack[_]

Consider the following attempt and variations thereof to track (nested) calls to Get (reasoning of this approach below):

Unprotect[Get];

trackGet[rule_] := (
Clear@Get;
i = 0; (* to prevent infinite recursion in some of the broken attempts*)
Hold[ (* hold definition until it has been modified *)
Get[file_] := With[
{s = Stack[_]},
wrapper[Get[Echo[file]]] /; ++i <= 3 (* prevent infinite recursion *) &&
(
Echo@Length@EchoFunction[Shallow]@s < 3 ||
(* the core part: check the stack entry three layers up
to prevent calling ourselves *)
s[[-3]] =!= HoldForm[wrapper[Get[Echo[file]]]]
)
]
] /. rule // ReleaseHold (* modify definition to show different cases *)
)

Print@"Version 1";
trackGet[wrapper[x_] :> Block[{}, x]]
<< GeneralUtilities

Print@"Version 2";
trackGet[wrapper[x_] :> With[{}, x]]
<< GeneralUtilities

Print@"Version 3";
trackGet[wrapper[x_] :> x]
<< GeneralUtilities

(* Version 1 *)
(* {Get[<<1>>],With[<<2>>]} *)
(* 2 *)
(* GeneralUtilities *)
(* {Block[<<2>>],Get[<<1>>],With[<<2>>]} *)
(* 3 *)
(* {Block[<<2>>],Get[<<1>>],Get[<<1>>],With[<<2>>]} *)
(* 4 *)
(* GeneralUtilitiesLoader *)

(* Version 2 *)
(* {Get[<<1>>],With[<<2>>]} *)
(* 2 *)
(* GeneralUtilities *)
(* {Get[<<1>>],With[<<2>>]} *)
(* 2 *)
(* GeneralUtilities *)
(* {Get[<<1>>],With[<<2>>]} *)
(* 2 *)
(* GeneralUtilities *)

(* Version 3 *)
(* {Get[<<1>>],With[<<2>>]} *)
(* 2 *)
(* GeneralUtilities *)
(* {Get[<<1>>],With[<<2>>]} *)
(* 2 *)
(* GeneralUtilities *)
(* {Get[<<1>>],With[<<2>>]} *)
(* 2 *)
(* GeneralUtilities *)


First a bit about what I try to achieve with this: The goal is to track any call to Get[_] - but since I need to track nested calls, I can't use the Villegas-Gayley trick. The idea of this approach is to inspect the last few layers of the Stack[_] to prevent the function from directly calling itself (but deeper nested calls are allowed again).

A bit on trackGet from above: The general structure is

Get[file_] := With[
{s = Stack[_]},
wrapper[Get[Echo[file]]]/;(*stackCheck*)
]


For the three versions, the wrapper is either: Block[{}, ...] (Version 1), With[{}, ...] (Version 2), or nothing (Version 3).

The question now: Why do the outputs of the three version differ in the way they do, i.e. why does an empty Block work, but With or nothing does not?

A few observations:

• The wrapper is inside the Condition, so any of the special handling of lhs_:>With[{vars},res/;cond] does not apply. (And if it did, Block and With should both work according to the documentation)
• The stack returned by Stack[_] only sees my definition of Get, everything else is not shown.
• The stack only gets longer for the case with Block, the other two are always the same length (see the numbers in the output) - this seems to be the reason why only the Block variant works
• Update: I did a few more tests and it seems most functions work in place of Block (e.g. CompoundExpression) - the only exceptions I've found so far is no wrapper and With. I'm assuming that somehow "trivial" definitions are not shown in the stack (for system functions at least), where "trivial" seems to be able to handle With transparently, but not Block or other functions
• Look at InternalHandlerBlock if you just want to track Get – b3m2a1 Apr 16 '18 at 15:59
• @b3m2a1 Thanks a lot! This solves my original problem perfectly. But I'm still curios whether anyone knows what's going on in my attempt :) – Lukas Lang Apr 16 '18 at 16:41