Every Bitcoin transaction has inputs and outputs. In a simple transaction you might take a single transaction input and create two outputs: one to the receiver and one to your new address. Each of the inputs can be traced all the way back to the genesis of the coins (when they were mined). So for each input you can create a graph of addresses that "touched" the coins.
By creating new addresses for every transaction you have a pool of addresses that are associated with you. Importantly if any one of these addresses are associated with your identity all others can be associated with some degree of certainty.
For example, if you send me a transaction I can see the input that was spent (from the sending address), and I can see all the transaction outputs. If there are only 2 outputs (one to me, one to you) I can determine the new address you're using, and see when it sends a transaction, and to where. The new addresses you create don't give you anonymity, they reduce the statistical certainty that you control assets based on how far they are from an address associated with you.
With ZKP it's not possible to construct a graph of addresses that touched inputs using network data. The transaction inputs are not publicly revealed, instead a nullifier is created in ZK for each transaction input and revealed. The transaction is valid if the nullifiers have not been seen before. It's not possible to determine what inputs have or have not been spent.
There are other attacks that apply to both schemes such as looking at wallet specific transaction formatting/metadata or transaction origin ip, but these are not inherent to the protocols.