An island is a run of consecutive rows, such as days a product was out of stock. A gap is a hole between them. The classic trick finds islands with ROW_NUMBER:
SELECT product_id, day,
day - CAST(ROW_NUMBER() OVER (PARTITION BY product_id ORDER BY day) AS INTEGER) AS island
FROM stock_levels
WHERE units = 0;
Within a run of consecutive days, the date goes up by 1 and the row number goes up by 1, so day - row_number stays the same. A missing day makes the date jump while the row number does not, which starts a new value. Group by (product_id, island) to get each run's start, end and length.
Gaps are easier: compare each row with the previous one using LAG, and keep the places where the step is bigger than one day.